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,76 @@
stds.roblox = {
globals = {
"game",
"script",
string = {
fields = {
split = {},
},
},
},
read_globals = {
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof",
"delay", "time", "version",
"UserSettings",
-- Libraries
"debug",
"utf8",
math = {
"sign", "clamp",
"abs",
"acos",
"asin",
"atan",
"ceil",
"cos",
"deg",
"exp",
"floor",
"fmod",
"huge",
"log",
"max",
"min",
"modf",
"pi",
"rad",
"random",
"randomseed",
"sin",
"sqrt",
"tan",
},
-- Types
"Enum",
"Axes", "BrickColor", "CFrame", "Color3", "ColorSequence",
"ColorSequenceKeypoint", "Faces","Instance","NumberRange",
"NumberSequence", "NumberSequenceKeypoint", "PhysicalProperties",
"Ray", "Rect", "Region3", "Region3int16", "TweenInfo",
"UDim", "UDim2",
"Vector2", "Vector2int16", "Vector3", "Vector3int16",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
}
std = "lua51+roblox"
files["**/*.spec.lua"] = {
std = "+testez",
}
@@ -0,0 +1,17 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"MultiLineStatement": "disabled",
"DeprecatedGlobal": "fatal",
"LocalShadow": "disabled",
"LocalUnused": "disabled",
"FunctionUnused": "disabled",
"ImportUnused": "disabled",
"SameLineStatement": "disabled",
"ImplicitReturn": "disabled",
"UnknownGlobal": "fatal",
"GlobalUsedAsLocal": "fatal"
}
}
@@ -0,0 +1,487 @@
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local TeleportService = game:GetService("TeleportService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local LocalizationService = game:GetService("LocalizationService")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local HttpService = game:GetService("HttpService")
local create = require(RobloxGui:WaitForChild("Modules"):WaitForChild("Common"):WaitForChild("Create"))
local ErrorPrompt = require(RobloxGui.Modules.ErrorPrompt)
local Url = require(RobloxGui.Modules.Common.Url)
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
local fflagDebugEnableErrorStringTesting = game:DefineFastFlag("DebugEnableErrorStringTesting", false)
local fflagShouldMuteUnlocalizedError = game:DefineFastFlag("ShouldMuteUnlocalizedError", false)
-- After 2 hours, disable reconnect after the failure of first try
local fflagDisableReconnectAfterPotentialTimeout = game:DefineFastFlag("DisableReconnectAfterPotentialTimeout", false)
local fIntPotentialClientTimeout = game:DefineFastInt("PotentialClientTimeoutSeconds", 7200)
local LEAVE_GAME_FRAME_WAITS = 2
local function safeGetFInt(name, defaultValue)
local success, result = pcall(function() return tonumber(settings():GetFVariable(name)) end)
return success and result or defaultValue
end
local function safeGetFString(name, defaultValue)
local success, result = pcall(function() return settings():GetFVariable(name) end)
return success and result or defaultValue
end
local inGameGlobalGuiInset = safeGetFInt("InGameGlobalGuiInset", 36)
local defaultTimeoutTime = safeGetFInt("DefaultTimeoutTimeMs", 10000) / 1000
-- when this flag turns on, all the errors will not have reconnect option
local reconnectDisabled = settings():GetFFlag("ReconnectDisabled")
local reconnectDisabledReason = safeGetFString("ReconnectDisabledReason", "We're sorry, Roblox is temporarily unavailable. Please try again later.")
local lastErrorTimeStamp = tick()
local coreScriptTableTranslator = CoreGui.CoreScriptLocalization:GetTranslator(LocalizationService.RobloxLocaleId)
local errorPrompt
local graceTimeout = -1
local screenWidth = RobloxGui.AbsoluteSize.X
local ConnectionPromptState = {
NONE = 1, -- General Error Message
RECONNECT_PLACELAUNCH = 2, -- Show PlaceLaunching Reconnect Options
RECONNECT_DISCONNECT = 3, -- Show Disconnect Reconnect Options
TELEPORT_FAILED = 4, -- Show Teleport Failure Message
IS_RECONNECTING = 5, -- Show Reconnecting Animation
RECONNECT_DISABLED_DISCONNECT = 6, -- i.e After Player Being Kicked From Server
RECONNECT_DISABLED_PLACELAUNCH = 7, -- Unauthorized join
RECONNECT_DISABLED = 8, -- General Disable by FFlag, i.e overloaded servers
}
local connectionPromptState = ConnectionPromptState.NONE
-- error that triggers reconnection
local errorForReconnect = Enum.ConnectionError.OK
-- this will be loaded from localization table
local ErrorTitles = {
[ConnectionPromptState.RECONNECT_PLACELAUNCH] = "Join Error",
[ConnectionPromptState.RECONNECT_DISABLED_PLACELAUNCH] = "Join Error",
[ConnectionPromptState.RECONNECT_DISCONNECT] = "Disconnected",
[ConnectionPromptState.RECONNECT_DISABLED_DISCONNECT] = "Disconnected",
[ConnectionPromptState.TELEPORT_FAILED] = "Teleport Failed",
[ConnectionPromptState.RECONNECT_DISABLED] = "Error",
}
local ErrorTitleLocalizationKey = {
[ConnectionPromptState.RECONNECT_PLACELAUNCH] = "InGame.ConnectionError.Title.JoinError",
[ConnectionPromptState.RECONNECT_DISABLED_PLACELAUNCH] = "InGame.ConnectionError.Title.JoinError",
[ConnectionPromptState.RECONNECT_DISCONNECT] = "InGame.ConnectionError.Title.Disconnected",
[ConnectionPromptState.RECONNECT_DISABLED_DISCONNECT] = "InGame.ConnectionError.Title.Disconnected",
[ConnectionPromptState.TELEPORT_FAILED] = "InGame.ConnectionError.Title.TeleportFailed",
[ConnectionPromptState.RECONNECT_DISABLED] = "InGame.CommonUI.Title.Error",
}
-- only return success when a valid root id is given
local function fetchStarterPlaceId(universeId)
local apiPath = "v1/games"
local params = "universeIds="..universeId
local fullUrl = Url.GAME_URL..apiPath.."?"..params
local success, result = pcall(HttpRbxApiService.GetAsyncFullUrl, HttpRbxApiService, fullUrl)
if success then
local result = HttpService:JSONDecode(result)
if result and result["data"] and result["data"][1] then
local rootId = result["data"][1]["rootPlaceId"]
if rootId then
return true, rootId
end
end
end
return false, -1
end
-- Screengui holding the prompt and make it on top of blur
local screenGui = create 'ScreenGui' {
Parent = CoreGui,
Name = "RobloxPromptGui",
OnTopOfCoreBlur = true,
DisplayOrder = 9,
AutoLocalize = false,
}
-- semi-transparent frame overlay
local promptOverlay = create 'Frame' {
Name = 'promptOverlay',
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, inGameGlobalGuiInset),
Position = UDim2.new(0, 0, 0, -inGameGlobalGuiInset),
Active = false,
Parent = screenGui
}
-- Button Callbacks --
local reconnectFunction = function()
if connectionPromptState == ConnectionPromptState.IS_RECONNECTING then
return
end
AnalyticsService:ReportCounter("ReconnectPrompt-ReconnectActivated")
connectionPromptState = ConnectionPromptState.IS_RECONNECTING
errorPrompt:primaryShimmerPlay()
local fetchStarterPlaceSuccess, starterPlaceId
if game.GameId > 0 then
fetchStarterPlaceSuccess, starterPlaceId = fetchStarterPlaceId(game.GameId)
end
-- Wait for the remaining time (if there is any)
local currentTime = tick()
if currentTime < graceTimeout then
wait(graceTimeout - currentTime)
end
if fetchStarterPlaceSuccess and starterPlaceId > 0 then
TeleportService:Teleport(starterPlaceId)
else
TeleportService:Teleport(game.PlaceId)
end
end
local leaveFunction = function()
GuiService.SelectedCoreObject = nil
for i = 1, LEAVE_GAME_FRAME_WAITS do
RunService.RenderStepped:wait()
end
game:Shutdown()
end
local closePrompt = function()
GuiService:ClearError()
end
-- Button Callbacks --
-- Reconnect Disabled List
local reconnectDisabledList = {
[Enum.ConnectionError.DisconnectLuaKick] = true,
[Enum.ConnectionError.DisconnectSecurityKeyMismatch] = true,
[Enum.ConnectionError.DisconnectNewSecurityKeyMismatch] = true,
[Enum.ConnectionError.DisconnectDuplicateTicket] = true,
[Enum.ConnectionError.DisconnectWrongVersion] = true,
[Enum.ConnectionError.DisconnectProtocolMismatch] = true,
[Enum.ConnectionError.DisconnectBadhash] = true,
[Enum.ConnectionError.DisconnectIllegalTeleport] = true,
[Enum.ConnectionError.DisconnectDuplicatePlayer] = true,
[Enum.ConnectionError.DisconnectCloudEditKick] = true,
[Enum.ConnectionError.DisconnectOnRemoteSysStats] = true,
[Enum.ConnectionError.DisconnectRaknetErrors] = true,
[Enum.ConnectionError.PlacelaunchFlooded] = true,
[Enum.ConnectionError.PlacelaunchHashException] = true,
[Enum.ConnectionError.PlacelaunchHashExpired] = true,
[Enum.ConnectionError.PlacelaunchUnauthorized] = true,
[Enum.ConnectionError.PlacelaunchUserLeft] = true,
[Enum.ConnectionError.PlacelaunchRestricted] = true,
}
local ButtonList = {
[ConnectionPromptState.RECONNECT_PLACELAUNCH] = {
{
Text = "Retry",
LocalizationKey = "InGame.CommonUI.Button.Retry",
LayoutOrder = 2,
Callback = reconnectFunction,
Primary = true
},
{
Text = "Cancel",
LocalizationKey = "Feature.SettingsHub.Action.CancelSearch",
LayoutOrder = 1,
Callback = leaveFunction,
}
},
[ConnectionPromptState.RECONNECT_DISCONNECT] = {
{
Text = "Reconnect",
LocalizationKey = "InGame.ConnectionError.Button.Reconnect",
LayoutOrder = 2,
Callback = reconnectFunction,
Primary = true,
},
{
Text = "Leave",
LocalizationKey = "Feature.SettingsHub.Label.LeaveButton",
LayoutOrder = 1,
Callback = leaveFunction,
}
},
[ConnectionPromptState.TELEPORT_FAILED] = {
{
Text = "OK",
LocalizationKey = "InGame.CommonUI.Button.Ok",
LayoutOrder = 1,
Callback = closePrompt,
Primary = true,
}
},
[ConnectionPromptState.RECONNECT_DISABLED_DISCONNECT] = {
{
Text = "Leave",
LocalizationKey = "Feature.SettingsHub.Label.LeaveButton",
LayoutOrder = 1,
Callback = leaveFunction,
Primary = true,
}
},
[ConnectionPromptState.RECONNECT_DISABLED_PLACELAUNCH] = {
{
Text = "Leave",
LocalizationKey = "Feature.SettingsHub.Label.LeaveButton",
LayoutOrder = 1,
Callback = leaveFunction,
Primary = true,
}
},
[ConnectionPromptState.RECONNECT_DISABLED] = {
{
Text = "Leave",
LocalizationKey = "Feature.SettingsHub.Label.LeaveButton",
LayoutOrder = 1,
Callback = leaveFunction,
Primary = true,
}
}
}
local updateFullScreenEffect = {
[ConnectionPromptState.NONE] = function()
RunService:SetRobloxGuiFocused(false)
promptOverlay.Active = false
promptOverlay.Transparency = 1
end,
[ConnectionPromptState.RECONNECT_DISCONNECT] = function()
RunService:SetRobloxGuiFocused(true)
promptOverlay.Active = true
promptOverlay.Transparency = 1
end,
[ConnectionPromptState.RECONNECT_PLACELAUNCH] = function()
RunService:SetRobloxGuiFocused(false)
promptOverlay.Active = true
promptOverlay.Transparency = 0.3
end,
[ConnectionPromptState.TELEPORT_FAILED] = function()
RunService:SetRobloxGuiFocused(false)
promptOverlay.Active = true
promptOverlay.Transparency = 0.3
end,
[ConnectionPromptState.RECONNECT_DISABLED_DISCONNECT] = function()
RunService:SetRobloxGuiFocused(true)
promptOverlay.Active = true
promptOverlay.Transparency = 1
end,
[ConnectionPromptState.RECONNECT_DISABLED_PLACELAUNCH] = function()
RunService:SetRobloxGuiFocused(false)
promptOverlay.Active = true
promptOverlay.Transparency = 0.3
end,
[ConnectionPromptState.RECONNECT_DISABLED] = function()
RunService:SetRobloxGuiFocused(true)
promptOverlay.Active = true
promptOverlay.Transparency = 1
end,
}
local function onEnter(newState)
if not errorPrompt then
local extraConfiguration = {
MenuIsOpenKey = "ConnectionErrorPrompt",
PlayAnimation = not fflagDebugEnableErrorStringTesting
}
errorPrompt = ErrorPrompt.new("Default", extraConfiguration)
errorPrompt:setParent(promptOverlay)
errorPrompt:resizeWidth(screenWidth)
end
if updateFullScreenEffect[newState] then
updateFullScreenEffect[newState]()
end
errorPrompt:setErrorTitle(ErrorTitles[newState], ErrorTitleLocalizationKey[newState])
errorPrompt:updateButtons(ButtonList[newState])
end
local function onExit(oldState)
if oldState == ConnectionPromptState.IS_RECONNECTING then
errorPrompt:primaryShimmerStop()
end
end
-- state transit function
local function stateTransit(errorType, errorCode, oldState)
if errorType == Enum.ConnectionError.OK then
return ConnectionPromptState.NONE
end
if oldState == ConnectionPromptState.NONE then
if reconnectDisabled then
return ConnectionPromptState.RECONNECT_DISABLED
end
lastErrorTimeStamp = tick()
if errorType == Enum.ConnectionError.DisconnectErrors then
-- reconnection will be delayed after graceTimeout
graceTimeout = tick() + defaultTimeoutTime
errorForReconnect = Enum.ConnectionError.DisconnectErrors
if reconnectDisabledList[errorCode] then
return ConnectionPromptState.RECONNECT_DISABLED_DISCONNECT
end
AnalyticsService:ReportCounter("ReconnectPrompt-Disconnect")
return ConnectionPromptState.RECONNECT_DISCONNECT
elseif errorType == Enum.ConnectionError.PlacelaunchErrors then
errorForReconnect = Enum.ConnectionError.PlacelaunchErrors
if reconnectDisabledList[errorCode] then
return ConnectionPromptState.RECONNECT_DISABLED_PLACELAUNCH
end
AnalyticsService:ReportCounter("ReconnectPrompt-PlaceLaunch")
return ConnectionPromptState.RECONNECT_PLACELAUNCH
elseif errorType == Enum.ConnectionError.TeleportErrors then
AnalyticsService:ReportCounter("ReconnectPrompt-TeleportFailed")
return ConnectionPromptState.TELEPORT_FAILED
end
end
if oldState == ConnectionPromptState.IS_RECONNECTING then
-- if is reconnecting, then it is the reconnect failure
AnalyticsService:ReportCounter("ReconnectPrompt-ReconnectFailed")
if errorType == Enum.ConnectionError.TeleportErrors then
if fflagDisableReconnectAfterPotentialTimeout then
-- disable reconnect at second try after a long period of time since last error pops up.
if tick() > lastErrorTimeStamp + fIntPotentialClientTimeout then
if errorForReconnect == Enum.ConnectionError.PlacelaunchErrors then
return ConnectionPromptState.RECONNECT_DISABLED_PLACELAUNCH
else
return ConnectionPromptState.RECONNECT_DISABLED_DISCONNECT
end
end
end
if errorForReconnect == Enum.ConnectionError.PlacelaunchErrors then
return ConnectionPromptState.RECONNECT_PLACELAUNCH
elseif errorForReconnect == Enum.ConnectionError.DisconnectErrors then
return ConnectionPromptState.RECONNECT_DISCONNECT
end
end
end
return oldState
end
-- Look up in corelocalization for new string. Otherwise fallback to the original string
-- If it is teleport error but not TELEPORT_FAILED, use general string "Reconnect failed."
local function getErrorString(errorMsg, errorCode, reconnectError)
if errorCode == Enum.ConnectionError.OK then
return ""
end
if reconnectError then
local success, attemptTranslation = pcall(function()
return coreScriptTableTranslator:FormatByKey("InGame.ConnectionError.ReconnectFailed")
end)
if success then return attemptTranslation end
return "Reconnect was unsuccessful. Please try again."
end
if errorCode == Enum.ConnectionError.DisconnectLuaKick then
return errorMsg
end
local key = string.gsub(tostring(errorCode), "Enum", "InGame")
if coreScriptTableTranslator then
local success, attemptTranslation = pcall(function()
if errorCode == Enum.ConnectionError.DisconnectIdle then
return coreScriptTableTranslator:FormatByKey(key, {RBX_NUM=tostring(20)})
end
return coreScriptTableTranslator:FormatByKey(key)
end)
-- Mute errors for jv app if they are not successfully translated
if not success and fflagShouldMuteUnlocalizedError then
local successUnknownError, localizedUnknownError = pcall(function()
return coreScriptTableTranslator:FormatByKey("InGame.ConnectionError.UnknownError")
end)
return successUnknownError and localizedUnknownError or ""
end
if success then return attemptTranslation end
end
return errorMsg
end
local function updateErrorPrompt(errorMsg, errorCode, errorType)
local newPromptState = stateTransit(errorType, errorCode, connectionPromptState)
if newPromptState ~= connectionPromptState then
onExit(connectionPromptState)
connectionPromptState = newPromptState
onEnter(newPromptState)
end
if errorType == Enum.ConnectionError.TeleportErrors and
connectionPromptState ~= ConnectionPromptState.TELEPORT_FAILED then
errorMsg = getErrorString(errorMsg, errorCode, true)
else
errorMsg = getErrorString(errorMsg, errorCode)
end
if connectionPromptState == ConnectionPromptState.RECONNECT_DISABLED then
errorMsg = reconnectDisabledReason
end
if errorPrompt then
errorPrompt:onErrorChanged(errorMsg, errorCode)
end
end
local function onErrorMessageChanged()
local errorMsg = GuiService:GetErrorMessage()
local errorCode = GuiService:GetErrorCode()
local errorType = GuiService:GetErrorType()
updateErrorPrompt(errorMsg, errorCode, errorType)
end
local function onScreenSizeChanged()
if not errorPrompt then
return
end
local newWidth = RobloxGui.AbsoluteSize.X
if screenWidth ~= newWidth then
screenWidth = newWidth
errorPrompt:resizeWidth(screenWidth)
end
end
local function onLocaleIdChanged()
coreScriptTableTranslator = CoreGui.CoreScriptLocalization:GetTranslator(LocalizationService.RobloxLocaleId)
end
-- only when we load this script from engine (engine feature LoadErrorHandlerFromEngine is enabled)
-- stop the connection for xBox as its handler is loaded in loadingscript.lua
local loadErrorHandlerFromEngine = game:GetEngineFeature("LoadErrorHandlerFromEngine")
local shouldSetUpErrorHandlerFromThisScript = not (loadErrorHandlerFromEngine and GuiService:IsTenFootInterface())
if shouldSetUpErrorHandlerFromThisScript then
RobloxGui:GetPropertyChangedSignal("AbsoluteSize"):connect(onScreenSizeChanged)
LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"):connect(onLocaleIdChanged)
-- pre-run it once in case some error occurs before the connection
onErrorMessageChanged()
GuiService.ErrorMessageChanged:connect(onErrorMessageChanged)
if fflagDebugEnableErrorStringTesting then
local testingSet = require(RobloxGui.Modules.ErrorTestSets)
for errorType, errorList in pairs(testingSet) do
for _, errorCode in pairs(errorList) do
updateErrorPrompt("Should show localized strings, please file a jira ticket for missing translation.", errorCode, errorType)
wait(2)
connectionPromptState = ConnectionPromptState.NONE
end
end
end
end
@@ -0,0 +1,5 @@
{
"lint": {
"SameLineStatement": "fatal"
}
}
@@ -0,0 +1,162 @@
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local NotificationService = game:GetService("NotificationService")
local HttpService = game:GetService("HttpService")
local HttpRbxApiService = game:GetService("HttpRbxApiService")
local ErrorPrompt = require(RobloxGui.Modules.ErrorPrompt)
local Url = require(RobloxGui.Modules.Common.Url)
local function leaveGame()
GuiService.SelectedCoreObject = nil
game:Shutdown()
end
local antiAddictionState = {
Warning = 1,
Lockout = 2
}
local function parseResponse(responseTable)
local state = responseTable["response"]["state"]["type"]
local messages = responseTable["response"]["state"]["messages"]
return {
State = state,
Messages = messages,
}
end
local function markRead(messageId)
local apiPath = "screen-time-api/v1/messages/mark-read"
local params = "mesasgeId=" .. messageId
local fullUrl = Url.RCS_URL..apiPath.."?"..params
local success, result = pcall(HttpRbxApiService.GetAsyncFullUrl, HttpRbxApiService, fullUrl)
end
--[[
Resolving message:
* display (move message PendingResolve -> Displaying)
* markread - spawned as soon as displayed to resolve with server
* user input - pressing "ok" (move message Displaying -> Resolved)
]]
local messageQueue = {
pendingResolveMessage = {},
displaying = nil,
resolvedMessage = {},
displayMessageCallback = nil,
update = function(self, newMessageList)
for _, message in pairs(newMessageList) do
local pendingMessage = {
id = message["id"],
message = message["text"]
}
table.insert(self.pendingResolveMessage, pendingMessage)
end
self:processNext()
end,
processNext = function(self)
if self.displaying then
return
end
local messageToDisplay = table.remove(self.pendingResolveMessage, 1)
if not messageToDisplay then
return
end
if self.resolvedMessage[messageToDisplay.id] then
self:processNext()
return
end
self.displaying = messageToDisplay
if self.displayMessageCallback then
self.displayMessageCallback(messageToDisplay.message)
end
spawn(function()
markRead(messageToDisplay.id)
end)
end,
-- currently messages will be resolved on client side by user click on the "OK" button
resolve = function(self)
self.resolvedMessage[self.displaying.id] = self.displaying.message
self.displaying = nil
self:processNext()
end,
}
-- Setting up the prompt and connect callbacks
local extraConfiguration = {
MessageTextScaled = true,
HideErrorCode = true,
MenuIsOpenKey = "AnitAddictionPrompt",
}
local prompt = ErrorPrompt.new("Default", extraConfiguration)
prompt:setParent(RobloxGui)
local function displayMessage(message)
prompt:_open(message)
end
local function resolveMessage()
prompt:_close()
messageQueue:resolve()
end
local buttonList = {
{
Text = "OK",
LocalizationKey = "InGame.CommonUI.Button.Ok",
LayoutOrder = 1,
Callback = resolveMessage,
Primary = true,
}
}
messageQueue.displayMessageCallback = displayMessage
prompt:updateButtons(buttonList)
prompt:setErrorTitle("Warning", "InGame.CommonUI.Title.Warning")
local screenWidth = RobloxGui.AbsoluteSize.X
local function onScreenSizeChanged()
if not prompt then
return
end
local newWidth = RobloxGui.AbsoluteSize.X
if screenWidth ~= newWidth then
screenWidth = newWidth
prompt:resizeWidth(screenWidth)
end
end
RobloxGui:GetPropertyChangedSignal("AbsoluteSize"):connect(onScreenSizeChanged)
onScreenSizeChanged()
-- Setting up the prompt and connect callbacks
local antiAddictionUpdatedConnection
local function antiAddictionStatesUpdated(responseTable)
local response = parseResponse(responseTable)
-- immediately quit when locked out
if response.State == antiAddictionState.Lockout then
if antiAddictionUpdatedConnection then
antiAddictionUpdatedConnection:Disconnect()
end
leaveGame()
end
messageQueue:update(response.Messages)
end
antiAddictionUpdatedConnection = NotificationService.RobloxEventReceived:Connect(function(eventData)
if eventData.namespace == "AntiAddictionNotifications" then
local responseTable = HttpService:JSONDecode(eventData.detail)
antiAddictionStatesUpdated(responseTable)
end
end)
@@ -0,0 +1,485 @@
--[[
// FileName: AvatarContextMenu.lua
// Written by: TheGamer101
// Description: A context menu to allow users to click on avatars and then interact with that user.
]]
-- OPTIONS
local DEBUG_MODE = game:GetService("RunService"):IsStudio() -- use this to run as a guest/use in games that don't have AvatarContextMenu. FOR TESTING ONLY!
local isAvatarContextMenuEnabled = false
-- CONSTANTS
local MAX_CONTEXT_MENU_DISTANCE = 100
local OPEN_MENU_TIME = 0.2
local OPEN_MENU_TWEEN = TweenInfo.new(OPEN_MENU_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local CLOSE_MENU_TIME = 0.2
local CLOSE_MENU_TWEEN = TweenInfo.new(CLOSE_MENU_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
local LEAVE_MENU_ACTION_NAME = "EscapeAvatarContextMenu"
local GAMEPAD_OPEN_MENU_ACTION = "GamepadOpenAvatarContextMenu"
local SWITCH_PAGE_ACTION_NAME = "SwitchPageAvatarContextMenu"
local MAX_MOVEMENT_THRESHOLD = 20
-- SERVICES
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local CoreGuiService = game:GetService("CoreGui")
local StarterGui = game:GetService("StarterGui")
local GuiService = game:GetService("GuiService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local hasTrackedAvatarContextMenu = false
function enableAvatarContextMenu(enabled)
isAvatarContextMenuEnabled = not not enabled
if isAvatarContextMenuEnabled and not hasTrackedAvatarContextMenu then
hasTrackedAvatarContextMenu = true
AnalyticsService:TrackEvent("Game", "AvatarContextMenuEnabled", "placeId: " .. tostring(game.PlaceId))
end
end
StarterGui:RegisterSetCore("SetAvatarContextMenuEnabled", enableAvatarContextMenu)
StarterGui:RegisterSetCore("AvatarContextMenuEnabled", enableAvatarContextMenu)
StarterGui:RegisterGetCore("AvatarContextMenuEnabled", function()
return isAvatarContextMenuEnabled
end)
--- MODULES
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local AvatarMenuModules = CoreGuiModules:WaitForChild("AvatarContextMenu")
local ContextMenuGui = require(AvatarMenuModules:WaitForChild("ContextMenuGui"))
local ContextMenuItemsModule = require(AvatarMenuModules:WaitForChild("ContextMenuItems"))
local ContextMenuUtil = require(AvatarMenuModules:WaitForChild("ContextMenuUtil"))
local SelectedCharacterIndicator = require(AvatarMenuModules:WaitForChild("SelectedCharacterIndicator"))
local ThemeHandler = require(AvatarMenuModules.ThemeHandler)
local Backpack = require(CoreGuiModules.BackpackScript)
local EmotesMenuMaster = require(CoreGuiModules.EmotesMenu.EmotesMenuMaster)
local BlockingUtility = require(CoreGuiModules.BlockingUtility)
--- VARIABLES
local LocalPlayer = PlayersService.LocalPlayer
while not LocalPlayer do
PlayersService.PlayerAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
-- no avatar context menu for guests
if LocalPlayer.UserId <= 0 and not DEBUG_MODE then return end
local ContextMenuItems = nil
local ContextMenuFrame = nil
local ContextMenuOpening = false
local ContextMenuOpen = false
local SelectedPlayer = nil
local lastInputObject = nil
local initialScreenPoint = nil
local hasTouchSwipeInput = nil
local contextMenuPlayerChangedConn = nil
ContextMenuFrame = ContextMenuGui:CreateMenuFrame(ThemeHandler:GetTheme())
ContextMenuItems = ContextMenuItemsModule.new(ContextMenuFrame.Content.ContextActionList)
function SetSelectedPlayer(player, dontTween)
if SelectedPlayer == player then return end
SelectedPlayer = player
SelectedCharacterIndicator:ChangeSelectedPlayer(SelectedPlayer, ThemeHandler:GetTheme())
ContextMenuItems:BuildContextMenuItems(SelectedPlayer)
ContextMenuGui:SwitchToPlayerEntry(SelectedPlayer, dontTween)
end
function OpenMenu(theme)
ContextMenuOpening = true
ContextMenuFrame.Visible = true
ContextMenuFrame.Content.ContextActionList.CanvasPosition = Vector2.new(0,0)
ContextMenuFrame.Position = theme.OffScreenPosition
contextMenuPlayerChangedConn = ContextMenuGui.SelectedPlayerChanged:connect(function()
SetSelectedPlayer(ContextMenuGui:GetSelectedPlayer())
end)
ContextMenuFrame.Position = theme.OffScreenPosition
local positionTween = TweenService:Create(ContextMenuFrame, OPEN_MENU_TWEEN, {Position = theme.OnScreenPosition})
positionTween:Play()
positionTween.Completed:wait()
ContextMenuOpening = false
end
function BindMenuActions()
-- Close Menu actions
local closeMenuFunc = function(actionName, inputState, input)
if inputState ~= Enum.UserInputState.Begin then
return
end
CloseContextMenu()
end
ContextActionService:BindCoreAction(LEAVE_MENU_ACTION_NAME, closeMenuFunc, false, Enum.KeyCode.Escape,
Enum.KeyCode.ButtonB)
local gamepadSwitchPage = function(actionName, inputState, input)
if inputState == Enum.UserInputState.Begin then
if input.KeyCode == Enum.KeyCode.ButtonR1 then
ContextMenuGui:OffsetPlayerEntry(1)
elseif input.KeyCode == Enum.KeyCode.ButtonL1 then
ContextMenuGui:OffsetPlayerEntry(-1)
end
end
end
ContextActionService:BindCoreAction(SWITCH_PAGE_ACTION_NAME, gamepadSwitchPage, false, Enum.KeyCode.ButtonR1,
Enum.KeyCode.ButtonL1)
local menuOpenedCon = nil
menuOpenedCon = GuiService.MenuOpened:connect(function()
menuOpenedCon:disconnect()
closeMenuFunc(nil, Enum.UserInputState.Begin, nil)
end)
end
function BuildPlayerCarousel(selectedPlayer, worldPoint)
local playersByProximity = {}
local players = PlayersService:GetPlayers()
for i = 1, #players do
if players[i].UserId > 0 or DEBUG_MODE then
if players[i] ~= LocalPlayer then
local playerPosition = ContextMenuUtil:GetPlayerPosition(players[i])
if playerPosition then
local distanceFromClicked = (worldPoint - playerPosition).magnitude
table.insert(playersByProximity, {players[i], distanceFromClicked})
end
end
end
end
local function closestPlayerComp(playerA, playerB)
return playerA[2] > playerB[2]
end
table.sort(playersByProximity, closestPlayerComp)
ContextMenuGui:BuildPlayerCarousel(playersByProximity, ThemeHandler:GetTheme())
end
PlayersService.PlayerRemoving:connect(function(player)
if ContextMenuOpen and player ~= SelectedPlayer then
ContextMenuGui:RemovePlayerEntry(player)
end
end)
local function CloseOtherOpenCoreGui()
if EmotesMenuMaster:isOpen() then
EmotesMenuMaster:close()
end
if Backpack.IsOpen then
Backpack.OpenClose()
end
end
function OpenContextMenu(player, worldPoint)
if ContextMenuOpening or ContextMenuOpen or not isAvatarContextMenuEnabled then
return
end
ContextMenuOpen = true
CloseOtherOpenCoreGui()
BuildPlayerCarousel(player, worldPoint)
ContextMenuUtil:DisablePlayerMovement()
BindMenuActions()
SetSelectedPlayer(player, true)
ContextMenuGui:UpdateGuiTheme(ThemeHandler:GetTheme())
OpenMenu(ThemeHandler:GetTheme())
end
function CloseContextMenu()
GuiService.SelectedCoreObject = nil
ContextActionService:UnbindCoreAction(LEAVE_MENU_ACTION_NAME)
ContextActionService:UnbindCoreAction(SWITCH_PAGE_ACTION_NAME)
ContextMenuUtil:EnablePlayerMovement()
if contextMenuPlayerChangedConn then
contextMenuPlayerChangedConn:disconnect()
end
local positionTween = TweenService:Create(ContextMenuFrame, CLOSE_MENU_TWEEN, {Position = UDim2.new(0.5, 0, 1, ContextMenuFrame.AbsoluteSize.Y)})
positionTween:Play()
positionTween.Completed:wait()
ContextMenuFrame.Visible = false
SetSelectedPlayer(nil)
ContextMenuOpen = false
end
ContextMenuGui:SetCloseMenuFunc(CloseContextMenu)
ContextMenuItems:SetCloseMenuFunc(CloseContextMenu)
local function isPointInside(point, topLeft, bottomRight)
return (point.X >= topLeft.X and
point.X <= bottomRight.X and
point.Y >= topLeft.Y and
point.Y <= bottomRight.Y)
end
function PointInSwipeArea(screenPoint)
local topLeft = ContextMenuFrame.AbsolutePosition
local nameTag = ContextMenuFrame.Content:FindFirstChild("NameTag")
local bottomRight = Vector2.new(topLeft.x + ContextMenuFrame.AbsoluteSize.x, nameTag.AbsolutePosition.y + nameTag.AbsoluteSize.y)
return isPointInside(screenPoint, topLeft, bottomRight)
end
function LocalPlayerHasToolEquipped()
if not LocalPlayer.Character then return false end
for _, child in ipairs(LocalPlayer.Character:GetChildren()) do
if child:IsA("BackpackItem") then
return true
end
end
return false
end
function shouldIgnoreLocalCharacter()
if LocalPlayer.Character then
local head = LocalPlayer.Character:FindFirstChild("Head")
if head and head:IsA("BasePart") then
-- This will be true if the player is in first person.
return head.LocalTransparencyModifier >= 0.95
end
end
end
function clickedOnPoint(screenPoint)
local camera = workspace.CurrentCamera
if not camera then return end
if LocalPlayerHasToolEquipped() then return end
local ray = camera:ScreenPointToRay(screenPoint.X, screenPoint.Y)
ray = Ray.new(ray.Origin, ray.Direction * MAX_CONTEXT_MENU_DISTANCE)
local hitPart, hitPoint
if shouldIgnoreLocalCharacter() then
hitPart, hitPoint = workspace:FindPartOnRay(ray, LocalPlayer.Character, false, true)
else
hitPart, hitPoint = workspace:FindPartOnRay(ray, nil, false, true)
end
local player = ContextMenuUtil:FindPlayerFromPart(hitPart)
if player and ((DEBUG_MODE and player ~= LocalPlayer) or (player ~= LocalPlayer and player.UserId > 0)) then
if ContextMenuOpen then
SetSelectedPlayer(player)
else
OpenContextMenu(player, hitPoint)
end
elseif not player and ContextMenuOpen then
CloseContextMenu()
end
end
function OnUserInput(screenPoint, inputObject)
if inputObject.UserInputState == Enum.UserInputState.Begin and lastInputObject == nil then
lastInputObject = inputObject
initialScreenPoint = screenPoint
elseif lastInputObject == inputObject and inputObject.UserInputState == Enum.UserInputState.Change then
if (screenPoint - initialScreenPoint).magnitude > 5 then
lastInputObject = nil
initialScreenPoint = nil
end
elseif inputObject.UserInputState == Enum.UserInputState.End and lastInputObject == inputObject then
lastInputObject = nil
initialScreenPoint = nil
clickedOnPoint(screenPoint)
end
end
function OnMouseMoved(screenPoint)
if not ContextMenuOpen and lastInputObject and (screenPoint - initialScreenPoint).magnitude > MAX_MOVEMENT_THRESHOLD then
lastInputObject = nil
initialScreenPoint = nil
end
end
function trackTouchSwipeInput(inputObject)
if inputObject.UserInputType == Enum.UserInputType.Touch then
if not hasTouchSwipeInput and inputObject.UserInputState == Enum.UserInputState.Begin then
if PointInSwipeArea(inputObject.Position) then
hasTouchSwipeInput = inputObject
end
elseif hasTouchSwipeInput == inputObject and inputObject.UserInputState == Enum.UserInputState.End then
spawn(function()
hasTouchSwipeInput = nil
end)
end
end
end
local function functionProcessInput(inputObject, gameProcessedEvent)
trackTouchSwipeInput(inputObject)
if gameProcessedEvent then
if inputObject == lastInputObject then
lastInputObject = nil
end
return
end
if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or
inputObject.UserInputType == Enum.UserInputType.Touch then
OnUserInput(Vector2.new(inputObject.Position.X, inputObject.Position.Y), inputObject)
elseif inputObject.UserInputType == Enum.UserInputType.MouseMovement then
OnMouseMoved(Vector2.new(inputObject.Position.X, inputObject.Position.Y))
end
end
UserInputService.InputBegan:Connect(functionProcessInput)
UserInputService.InputChanged:Connect(functionProcessInput)
UserInputService.InputEnded:Connect(functionProcessInput)
UserInputService.TouchSwipe:Connect(function(swipeDir, numOfTouches, gameProcessedEvent)
if not gameProcessedEvent then return end
if not ContextMenuOpen then return end
if not hasTouchSwipeInput then return end
local offset = 0
if swipeDir == Enum.SwipeDirection.Left then
offset = 1
elseif swipeDir == Enum.SwipeDirection.Right then
offset = -1
end
if offset ~= 0 then
ContextMenuGui:OffsetPlayerEntry(offset)
SetSelectedPlayer(ContextMenuGui:GetSelectedPlayer())
end
end)
LocalPlayer.FriendStatusChanged:Connect(function(player, friendStatus)
if player and player == SelectedPlayer then
local isBlocked = BlockingUtility:IsPlayerBlockedByUserId(player.UserId)
ContextMenuItems:UpdateFriendButton(friendStatus, isBlocked)
end
end)
Backpack.StateChanged.Event:Connect(function(isBackpackOpen)
if isBackpackOpen and ContextMenuOpen then
CloseContextMenu()
end
end)
EmotesMenuMaster.EmotesMenuToggled.Event:Connect(function(isEmotesOpen)
if isEmotesOpen and ContextMenuOpen then
CloseContextMenu()
end
end)
function GetWorldPoint(player)
if player.Character then
local rootPart = player.Character:FindFirstChild("HumanoidRootPart")
if rootPart then
return rootPart.Position
end
end
if player ~= LocalPlayer then
return GetWorldPoint(LocalPlayer)
end
return Vector3.new(0, 0, 0)
end
StarterGui:RegisterGetCore("AvatarContextMenuTarget",
function()
return SelectedPlayer
end
)
StarterGui:RegisterSetCore("AvatarContextMenuTarget",
function(player)
local isPlayer = typeof(player) == "Instance" and player:IsA("Player")
if isPlayer then
if player.Parent ~= nil then
if ContextMenuOpen or ContextMenuOpening then
SetSelectedPlayer(player, true)
else
OpenContextMenu(player, GetWorldPoint(player))
end
else
error("AvatarContextMenuTarget Player must be in the game")
end
elseif player == nil then
CloseContextMenu()
else
error("AvatarContextMenuTarget argument must be a Player or nil")
end
end
)
local function getClosestPlayer()
if not LocalPlayer.Character then
return
end
local rootPart = LocalPlayer.Character:FindFirstChild("HumanoidRootPart")
if not rootPart then
return
end
local localPosition = rootPart.Position
local closestPlayer = nil
local closestPlayerDistance = math.huge
local closestPlayerPoint = nil
local players = PlayersService:GetPlayers()
for _, player in ipairs(players) do
if player ~= LocalPlayer and player.Character then
local rootPart = player.Character:FindFirstChild("HumanoidRootPart")
if rootPart and (rootPart.Position - localPosition).magnitude < closestPlayerDistance then
closestPlayer = player
closestPlayerDistance = (rootPart.Position - localPosition).magnitude
closestPlayerPoint = rootPart.Position
end
end
end
return closestPlayer, closestPlayerPoint
end
local function gamepadOpenMenu(actionName, inputState, input)
if not isAvatarContextMenuEnabled then
return Enum.ContextActionResult.Pass
end
if GuiService.SelectedCoreObject or GuiService.SelectedObject then
return Enum.ContextActionResult.Pass
end
if inputState ~= Enum.UserInputState.Begin then
return Enum.ContextActionResult.Sink
end
if ContextMenuOpen then
CloseContextMenu()
else
local closestPlayer, closestPlayerPoint = getClosestPlayer()
if closestPlayer then
OpenContextMenu(closestPlayer, closestPlayerPoint)
end
end
return Enum.ContextActionResult.Sink
end
ContextActionService:BindCoreAction(GAMEPAD_OPEN_MENU_ACTION, gamepadOpenMenu, false, Enum.KeyCode.DPadUp)
@@ -0,0 +1,194 @@
--[[
// Filename: BlockPlayerPrompt.lua
// Version 1.0
// Written by: TheGamer101
// Description: Handles prompting the blocking and unblocking of Players.
]]--
local StarterGui = game:GetService("StarterGui")
local PlayersService = game:GetService("Players")
local CoreGuiService = game:GetService("CoreGui")
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local LocalPlayer = PlayersService.LocalPlayer
while LocalPlayer == nil do
PlayersService.ChildAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local PromptCreator = require(CoreGuiModules:WaitForChild("PromptCreator"))
local SocialUtil = require(CoreGuiModules:WaitForChild("SocialUtil"))
local BlockingUtility = require(CoreGuiModules.BlockingUtility)
-- fetch and store player block list
BlockingUtility:InitBlockListAsync()
local GetFFlagUseThumbnailUrl = require(RobloxGui.Modules.Common.Flags.GetFFlagUseThumbnailUrl)
local LegacyThumbnailUrls = require(CoreGuiModules.Common.LegacyThumbnailUrls)
local THUMBNAIL_SIZE = 200
local BUST_THUMBNAIL_SIZE = 420
local THUMBNAIL_URL = LegacyThumbnailUrls.Thumbnail
local BUST_THUMBNAIL_URL = LegacyThumbnailUrls.Bust
local REGULAR_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size150x150
local CONSOLE_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size352x352
local REGULAR_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.HeadShot
local CONSOLE_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.AvatarThumbnail
function createFetchImageFunction(...)
local args = {...}
return function(imageLabel)
spawn(function()
local imageUrl = SocialUtil.GetPlayerImage(unpack(args))
if imageLabel and imageLabel.Parent then
imageLabel.Image = imageUrl
end
end)
end
end
function DoPromptBlockPlayer(playerToBlock)
if BlockingUtility:IsPlayerBlockedByUserId(playerToBlock.UserId) then
return
end
local thumbnailUrl = BUST_THUMBNAIL_URL:format(BUST_THUMBNAIL_SIZE, BUST_THUMBNAIL_SIZE, playerToBlock.UserId)
local thumbnailUrlConsole = THUMBNAIL_URL:format(THUMBNAIL_SIZE, THUMBNAIL_SIZE, playerToBlock.UserId)
if GetFFlagUseThumbnailUrl() then
-- we use createFetchImageFunction which sets the image in the prompt, these are useless
thumbnailUrl = ""
thumbnailUrlConsole = ""
end
local function promptCompletedCallback(clickedConfirm)
if clickedConfirm then
local successfullyBlocked = BlockingUtility:BlockPlayerAsync(playerToBlock)
if not successfullyBlocked then
while PromptCreator:IsCurrentlyPrompting() do
wait()
end
PromptCreator:CreatePrompt({
WindowTitle = "Error Blocking Player",
MainText = string.format("An error occurred while blocking %s. Please try again later.", playerToBlock.Name),
ConfirmationText = "Okay",
CancelActive = false,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToBlock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToBlock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
StripeColor = Color3.fromRGB(183, 34, 54),
})
end
end
end
PromptCreator:CreatePrompt({
WindowTitle = "Confirm Block",
MainText = string.format("Are you sure you want to block %s?", playerToBlock.Name),
ConfirmationText = "Block",
CancelText = "Cancel",
CancelActive = true,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToBlock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToBlock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
PromptCompletedCallback = promptCompletedCallback,
})
end
function PromptBlockPlayer(player)
if LocalPlayer.UserId < 0 then
error("PromptBlockPlayer can not be called for guests!")
end
if typeof(player) == "Instance" and player:IsA("Player") then
if player.UserId < 0 then
error("PromptBlockPlayer can not be called on guests!")
end
if player == LocalPlayer then
error("PromptBlockPlayer: A user can not block themselves!")
end
DoPromptBlockPlayer(player)
else
error("Invalid argument to PromptBlockPlayer")
end
end
function DoPromptUnblockPlayer(playerToUnblock)
if not BlockingUtility:IsPlayerBlockedByUserId(playerToUnblock.UserId) and false then
return
end
local thumbnailUrl = BUST_THUMBNAIL_URL:format(BUST_THUMBNAIL_SIZE, BUST_THUMBNAIL_SIZE, playerToUnblock.UserId)
local thumbnailUrlConsole = THUMBNAIL_URL:format(THUMBNAIL_SIZE, THUMBNAIL_SIZE, playerToUnblock.UserId)
if GetFFlagUseThumbnailUrl() then
-- we use createFetchImageFunction which sets the image in the prompt, these are useless
thumbnailUrl = ""
thumbnailUrlConsole = ""
end
local function promptCompletedCallback(clickedConfirm)
if clickedConfirm then
local successfullyUnblocked = BlockingUtility:UnblockPlayerAsync(playerToUnblock)
if not successfullyUnblocked then
while PromptCreator:IsCurrentlyPrompting() do
wait()
end
PromptCreator:CreatePrompt({
WindowTitle = "Error Unblocking Player",
MainText = string.format("An error occurred while unblocking %s. Please try again later.", playerToUnblock.Name),
ConfirmationText = "Okay",
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToUnblock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnblock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
StripeColor = Color3.fromRGB(183, 34, 54),
})
end
end
end
PromptCreator:CreatePrompt({
WindowTitle = "Confirm Unblock",
MainText = string.format("Would you like to unblock %s?", playerToUnblock.Name),
ConfirmationText = "Unblock",
CancelText = "Cancel",
CancelActive = true,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToUnblock.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnblock.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
PromptCompletedCallback = promptCompletedCallback,
})
end
function PromptUnblockPlayer(player)
if LocalPlayer.UserId < 0 then
error("PromptUnblockPlayer can not be called for guests!")
end
if typeof(player) == "Instance" and player:IsA("Player") then
if player.UserId < 0 then
error("PromptUnblockPlayer can not be called on guests!")
end
if player == LocalPlayer then
error("PromptUnblockPlayer: A user can not block themselves!")
end
DoPromptUnblockPlayer(player)
else
error("Invalid argument to PromptUnblockPlayer")
end
end
function GetBlockedUserIds()
if LocalPlayer.UserId < 0 then
error("GetBlockedUserIds can not be called for guests!")
end
return BlockingUtility:GetBlockedUserIdsAsync()
end
StarterGui:RegisterSetCore("PromptBlockPlayer", PromptBlockPlayer)
StarterGui:RegisterSetCore("PromptUnblockPlayer", PromptUnblockPlayer)
StarterGui:RegisterGetCore("GetBlockedUserIds", GetBlockedUserIds)
@@ -0,0 +1,384 @@
-- ContextActionTouch.lua
-- Copyright ROBLOX 2014, created by Ben Tkacheff
-- this script controls ui and firing of lua functions that are bound in ContextActionService for touch inputs
-- Essentially a user can bind a lua function to a key code, input type (mousebutton1 etc.) and this
-- Variables
local contextActionService = game:GetService("ContextActionService")
local userInputService = game:GetService("UserInputService")
local playersService = game:GetService("Players")
local isTouchDevice = userInputService.TouchEnabled
local functionTable = {}
local buttonVector = {}
local buttonScreenGui = nil
local buttonFrame = nil
local ContextDownImage = "https://www.roblox.com/asset/?id=97166756"
local ContextUpImage = "https://www.roblox.com/asset/?id=97166444"
local oldTouches = {}
local FFlagCancelButtonTouchEventOnMouseDragOff = game:DefineFastFlag("CancelButtonTouchEventOnMouseDragOff", false)
local buttonPositionTable = {
[1] = UDim2.new(0,123,0,70),
[2] = UDim2.new(0,30,0,60),
[3] = UDim2.new(0,180,0,160),
[4] = UDim2.new(0,85,0,-25),
[5] = UDim2.new(0,185,0,-25),
[6] = UDim2.new(0,185,0,260),
[7] = UDim2.new(0,216,0,65)
}
local maxButtons = #buttonPositionTable
-- Preload images
game:GetService("ContentProvider"):Preload(ContextDownImage)
game:GetService("ContentProvider"):Preload(ContextUpImage)
local localPlayer = playersService.LocalPlayer
while not localPlayer do
playersService.ChildAdded:wait()
localPlayer = playersService.LocalPlayer
end
local playerGui = localPlayer:WaitForChild("PlayerGui")
function createContextActionGui()
if not buttonScreenGui and isTouchDevice then
buttonScreenGui = Instance.new("ScreenGui")
buttonScreenGui.Name = "ContextActionGui"
buttonScreenGui.ResetOnSpawn = false
buttonFrame = Instance.new("Frame")
buttonFrame.BackgroundTransparency = 1
buttonFrame.Size = UDim2.new(0.3,0,0.5,0)
buttonFrame.Position = UDim2.new(0.7,0,0.5,0)
buttonFrame.Name = "ContextButtonFrame"
buttonFrame.Parent = buttonScreenGui
buttonFrame.Visible = not userInputService.ModalEnabled
userInputService.Changed:connect(function(property)
if property == "ModalEnabled" then
buttonFrame.Visible = not userInputService.ModalEnabled
end
end)
end
end
-- functions
function setButtonSizeAndPosition(object)
local buttonSize = 55
local xOffset = 10
local yOffset = 95
-- todo: better way to determine mobile sized screens
local onSmallScreen = (game:GetService("CoreGui").RobloxGui.AbsoluteSize.X < 600)
if not onSmallScreen then
buttonSize = 85
xOffset = 40
end
object.Size = UDim2.new(0,buttonSize,0,buttonSize)
end
-- deprecated functions to be removed when FFlagCancelButtonTouchEventOnMouseDragOff is removed
function contextButtonDown(button, inputObject, actionName)
if inputObject.UserInputType == Enum.UserInputType.Touch then
button.Image = ContextDownImage
contextActionService:CallFunction(actionName, Enum.UserInputState.Begin, inputObject)
end
end
function contextButtonMoved(button, inputObject, actionName)
if inputObject.UserInputType == Enum.UserInputType.Touch then
button.Image = ContextDownImage
contextActionService:CallFunction(actionName, Enum.UserInputState.Change, inputObject)
end
end
function contextButtonUp(button, inputObject, actionName)
button.Image = ContextUpImage
if inputObject.UserInputType == Enum.UserInputType.Touch and inputObject.UserInputState == Enum.UserInputState.End then
contextActionService:CallFunction(actionName, Enum.UserInputState.End, inputObject)
end
end
function contextActionButtonDown(button, inputObject, actionName)
button.Image = ContextDownImage
contextActionService:CallFunction(actionName, Enum.UserInputState.Begin, inputObject)
end
function contextActionButtonMoved(button, inputObject, actionName)
button.Image = ContextDownImage
contextActionService:CallFunction(actionName, Enum.UserInputState.Change, inputObject)
end
function contextActionButtonUp(button, inputObject, actionName)
button.Image = ContextUpImage
contextActionService:CallFunction(actionName, Enum.UserInputState.End, inputObject)
end
function contextActionButtonCancel(button, inputObject, actionName)
button.Image = ContextUpImage
contextActionService:CallFunction(actionName, Enum.UserInputState.Cancel, inputObject)
end
function isSmallScreenDevice()
return game:GetService("GuiService"):GetScreenResolution().y <= 320
end
function createNewButton(actionName, functionInfoTable)
local contextButton = Instance.new("ImageButton")
contextButton.Name = "ContextActionButton"
contextButton.BackgroundTransparency = 1
contextButton.Size = UDim2.new(0,45,0,45)
contextButton.Active = true
if isSmallScreenDevice() then
contextButton.Size = UDim2.new(0,35,0,35)
end
contextButton.Image = ContextUpImage
contextButton.Parent = buttonFrame
if FFlagCancelButtonTouchEventOnMouseDragOff then
local currentButtonTouch = nil
local fingerIsTouchingAwayFromButton = false
--[[
there are three states for a button:
OFF currentButtonTouch == nil
ON currentButtonTouch ~= nil and fingerIsTouchingAwayFromButton == false
AWAY currentButtonTouch ~= nil and fingerIsTouchingAwayFromButton == true
AWAY is when the user begins a touch event on the button and drags off, but still maintains contact with the screen
ContextActionService callbacks are generated for the following transitions between states:
OFF -> ON Enum.UserInputState.Begin
ON -> OFF Enum.UserInputState.End
AWAY -> OFF Enum.UserInputState.Cancel
]]--
userInputService.InputEnded:connect(function(inputObject)
if inputObject.UserInputType ~= Enum.UserInputType.Touch then
return
end
if currentButtonTouch ~= inputObject then
return
end
if inputObject.UserInputState == Enum.UserInputState.End and fingerIsTouchingAwayFromButton then
-- transition AWAY -> OFF
contextActionButtonCancel(contextButton, inputObject, actionName)
end
currentButtonTouch = nil
fingerIsTouchingAwayFromButton = false
end)
contextButton.InputBegan:connect(function(inputObject)
if inputObject.UserInputType ~= Enum.UserInputType.Touch then
return
end
if inputObject.UserInputState == Enum.UserInputState.Begin then
-- transition OFF -> ON
currentButtonTouch = inputObject
fingerIsTouchingAwayFromButton = false
contextActionButtonDown(contextButton, inputObject, actionName)
elseif inputObject.UserInputState == Enum.UserInputState.Change then
if currentButtonTouch ~= inputObject then
return
end
-- transition AWAY -> ON
fingerIsTouchingAwayFromButton = false
end
end)
contextButton.InputEnded:connect(function(inputObject)
if inputObject.UserInputType ~= Enum.UserInputType.Touch then
return
end
if currentButtonTouch ~= inputObject then
return
end
if inputObject.UserInputState == Enum.UserInputState.End then
-- transition ON -> OFF
currentButtonTouch = nil
fingerIsTouchingAwayFromButton = false
contextActionButtonUp(contextButton, inputObject, actionName)
elseif inputObject.UserInputState == Enum.UserInputState.Change then
-- transition ON -> AWAY
fingerIsTouchingAwayFromButton = true
end
end)
contextButton.InputChanged:connect(function(inputObject)
if inputObject.UserInputType ~= Enum.UserInputType.Touch then
return
end
if currentButtonTouch ~= inputObject then
return
end
contextActionButtonMoved(contextButton, inputObject, actionName)
end)
else
local currentButtonTouch = nil
userInputService.InputEnded:connect(function ( inputObject )
oldTouches[inputObject] = nil
end)
contextButton.InputBegan:connect(function(inputObject)
if oldTouches[inputObject] then return end
if inputObject.UserInputState == Enum.UserInputState.Begin and currentButtonTouch == nil then
currentButtonTouch = inputObject
contextButtonDown(contextButton, inputObject, actionName)
end
end)
contextButton.InputChanged:connect(function(inputObject)
if oldTouches[inputObject] then return end
if currentButtonTouch ~= inputObject then return end
contextButtonMoved(contextButton, inputObject, actionName)
end)
contextButton.InputEnded:connect(function(inputObject)
if oldTouches[inputObject] then return end
if currentButtonTouch ~= inputObject then return end
currentButtonTouch = nil
oldTouches[inputObject] = true
contextButtonUp(contextButton, inputObject, actionName)
end)
end
local actionIcon = Instance.new("ImageLabel")
actionIcon.Name = "ActionIcon"
actionIcon.Position = UDim2.new(0.175, 0, 0.175, 0)
actionIcon.Size = UDim2.new(0.65, 0, 0.65, 0)
actionIcon.BackgroundTransparency = 1
if functionInfoTable["image"] and type(functionInfoTable["image"]) == "string" then
actionIcon.Image = functionInfoTable["image"]
end
actionIcon.Parent = contextButton
local actionTitle = Instance.new("TextLabel")
actionTitle.Name = "ActionTitle"
actionTitle.Size = UDim2.new(1,0,1,0)
actionTitle.BackgroundTransparency = 1
actionTitle.Font = Enum.Font.SourceSansBold
actionTitle.TextColor3 = Color3.new(1,1,1)
actionTitle.TextStrokeTransparency = 0
actionTitle.FontSize = Enum.FontSize.Size18
actionTitle.TextWrapped = true
actionTitle.Text = ""
if functionInfoTable["title"] and type(functionInfoTable["title"]) == "string" then
actionTitle.Text = functionInfoTable["title"]
end
actionTitle.Parent = contextButton
return contextButton
end
function createButton( actionName, functionInfoTable )
local button = createNewButton(actionName, functionInfoTable)
local position = nil
for i = 1,#buttonVector do
if buttonVector[i] == "empty" then
position = i
break
end
end
if not position then
position = #buttonVector + 1
end
if position > maxButtons then
return -- todo: let user know we have too many buttons already?
end
buttonVector[position] = button
functionTable[actionName]["button"] = button
button.Position = buttonPositionTable[position]
button.Parent = buttonFrame
if buttonScreenGui and buttonScreenGui.Parent == nil then
buttonScreenGui.Parent = playerGui
if not buttonFrame.Parent then
buttonFrame.Parent = buttonScreenGui
end
end
end
function removeAction(actionName)
if not functionTable[actionName] then return end
local actionButton = functionTable[actionName]["button"]
if actionButton then
actionButton.Parent = nil
for i = 1,#buttonVector do
if buttonVector[i] == actionButton then
buttonVector[i] = "empty"
break
end
end
actionButton:Destroy()
end
functionTable[actionName] = nil
end
function addAction(actionName,createTouchButton,functionInfoTable)
if functionTable[actionName] then
removeAction(actionName)
end
functionTable[actionName] = {functionInfoTable}
if createTouchButton and isTouchDevice then
createContextActionGui()
createButton(actionName, functionInfoTable)
end
end
-- Connections
contextActionService.BoundActionChanged:connect( function(actionName, changeName, changeTable)
if functionTable[actionName] and changeTable then
local button = functionTable[actionName]["button"]
if button then
if changeName == "image" then
button.ActionIcon.Image = changeTable[changeName]
elseif changeName == "title" then
button.ActionTitle.Text = changeTable[changeName]
elseif changeName == "description" then
-- todo: add description to menu
elseif changeName == "position" then
button.Position = changeTable[changeName]
end
end
end
end)
contextActionService.BoundActionAdded:connect( function(actionName, createTouchButton, functionInfoTable)
addAction(actionName, createTouchButton, functionInfoTable)
end)
contextActionService.BoundActionRemoved:connect( function(actionName, functionInfoTable)
removeAction(actionName)
end)
contextActionService.GetActionButtonEvent:connect( function(actionName)
if functionTable[actionName] then
contextActionService:FireActionButtonFoundSignal(actionName, functionTable[actionName]["button"])
end
end)
-- make sure any bound data before we setup connections is handled
local boundActions = contextActionService:GetAllBoundActionInfo()
for actionName, actionData in pairs(boundActions) do
addAction(actionName,actionData["createTouchButton"],actionData)
end
@@ -0,0 +1,107 @@
local ContentProvider = game:GetService("ContentProvider")
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage")
local RunService = game:GetService("RunService")
local ScriptContext = game:GetService("ScriptContext")
local UserInputService = game:GetService("UserInputService")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RateLimiter = require(RobloxGui.Modules.ErrorReporting.RateLimiter)
local PiiFilter = require(RobloxGui.Modules.ErrorReporting.PiiFilter)
local BacktraceReporter = require(CorePackages.ErrorReporters.Backtrace.BacktraceReporter)
game:DefineFastFlag("EnableCoreScriptBacktraceReporting", false)
game:DefineFastString("CoreScriptBacktraceErrorUploadToken", "")
game:DefineFastInt("CoreScriptBacktracePIIFilterEraseTimeoutSeconds", 5 * 60)
-- These values control how many times we report the same error in a given period.
-- For these defaults, we will report the same message + stack combination 5 times per
-- minute, and ignore subsequent occurrences.
game:DefineFastFlag("CoreScriptBacktraceRepeatedErrorRateLimiting", true)
game:DefineFastInt("CoreScriptBacktraceRepeatedErrorRateLimitCount", 5)
game:DefineFastInt("CoreScriptBacktraceRepeatedErrorRateLimitPeriod", 60)
-- How long to wait, in tenths of a second, between ticks of the rate limit clock.
-- Higher values will improve performance but may cause repeated errors to be ignored
-- beyond the settings above.
game:DefineFastInt("CoreScriptBacktraceRepeatedErrorRateLimitProcessIntervalTenths", 10)
-- We don't have a default for this fast string, so if it's the empty string we
-- know we're at the default and we can't do error reports.
if game:GetFastFlag("EnableCoreScriptBacktraceReporting") and game:GetFastString("CoreScriptBacktraceErrorUploadToken") ~= "" then
local staticAttributes = {
LocalVersion = RunService:GetRobloxVersion(),
BaseUrl = ContentProvider.BaseUrl,
PlaceId = game.PlaceId,
Platform = UserInputService:GetPlatform().Name,
}
local piiFilter = PiiFilter.new({
eraseTimeout = game:GetFastInt("CoreScriptBacktracePIIFilterEraseTimeoutSeconds"),
})
local useRateLimiting = game:GetFastFlag("CoreScriptBacktraceRepeatedErrorRateLimiting")
local rateLimiter
if useRateLimiting then
rateLimiter = RateLimiter.new({
period = game:GetFastInt("CoreScriptBacktraceRepeatedErrorRateLimitPeriod"),
threshold = game:GetFastInt("CoreScriptBacktraceRepeatedErrorRateLimitCount"),
processInterval = game:GetFastInt("CoreScriptBacktraceRepeatedErrorRateLimitProcessIntervalTenths") / 10,
})
end
local reporter = BacktraceReporter.new({
httpService = HttpService,
token = game:GetFastString("CoreScriptBacktraceErrorUploadToken"),
processErrorReportMethod = function(report)
report:addAttributes(staticAttributes)
local playerCount = #Players:GetPlayers()
report:addAttributes({
PlayerCount = playerCount,
})
return report
end,
})
local function handleErrorDetailed(message, stack, offendingScript, details)
if offendingScript ~= nil and (offendingScript:IsDescendantOf(CoreGui) or offendingScript:IsDescendantOf(CorePackages)) then
local cleanedMessage = piiFilter:cleanPii(message)
local cleanedStack = piiFilter:cleanPii(stack)
if details ~= nil then
details = piiFilter:cleanPii(details)
end
if useRateLimiting then
-- Details includes function args, so we don't include them in the error
-- signature. They'll still be included in the upload to Backtrace.
local signature = cleanedMessage .. cleanedStack
if rateLimiter:isRateLimited(signature) then
return
else
rateLimiter:increment(signature)
end
end
reporter:reportErrorDeferred(cleanedMessage, cleanedStack, details)
end
end
ScriptContext.ErrorDetailed:Connect(function(...)
local success, message = pcall(handleErrorDetailed, ...)
if not success then
warn(("CoreScript error reporter failed to handle an error:\n%s"):format(message))
end
end)
local serverVersionRemote = RobloxReplicatedStorage:WaitForChild("GetServerVersion", math.huge)
local serverVersion = serverVersionRemote:InvokeServer()
staticAttributes.ServerVersion = serverVersion
end
@@ -0,0 +1,302 @@
--[[
// Filename: FriendPlayerPrompt.lua
// Version 1.0
// Written by: TheGamer101
// Description: Can prompt a user to send a friend request or unfriend a player.
]]--
local StarterGui = game:GetService("StarterGui")
local PlayersService = game:GetService("Players")
local CoreGuiService = game:GetService("CoreGui")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local LocalPlayer = PlayersService.LocalPlayer
while LocalPlayer == nil do
PlayersService.ChildAdded:wait()
LocalPlayer = PlayersService.LocalPlayer
end
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local PromptCreator = require(CoreGuiModules:WaitForChild("PromptCreator"))
local SocialUtil = require(CoreGuiModules:WaitForChild("SocialUtil"))
local FriendingUtility = require(CoreGuiModules:WaitForChild("FriendingUtility"))
local FFlagFriendPlayerPromptUseFormatByKey = settings():GetFFlag('FriendPlayerPromptUseFormatByKey')
local RobloxTranslator = require(CoreGuiModules:WaitForChild("RobloxTranslator"))
local GetFFlagUseThumbnailUrl = require(RobloxGui.Modules.Common.Flags.GetFFlagUseThumbnailUrl)
local LegacyThumbnailUrls = require(CoreGuiModules.Common.LegacyThumbnailUrls)
local THUMBNAIL_SIZE = 200
local BUST_THUMBNAIL_SIZE = 420
local THUMBNAIL_URL = LegacyThumbnailUrls.Thumbnail
local BUST_THUMBNAIL_URL = LegacyThumbnailUrls.Bust
local REGULAR_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size150x150
local CONSOLE_THUMBNAIL_IMAGE_SIZE = Enum.ThumbnailSize.Size352x352
local REGULAR_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.HeadShot
local CONSOLE_THUMBNAIL_IMAGE_TYPE = Enum.ThumbnailType.AvatarThumbnail
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
local function LocalizedGetString(key, rtv)
pcall(function()
rtv = RobloxTranslator:FormatByKey(key)
end)
return rtv
end
function createFetchImageFunction(...)
local args = {...}
return function(imageLabel)
spawn(function()
local imageUrl = SocialUtil.GetPlayerImage(unpack(args))
if imageLabel and imageLabel.Parent then
imageLabel.Image = imageUrl
end
end)
end
end
function SendFriendRequest(playerToFriend)
AnalyticsService:ReportCounter("FriendPlayerPrompt-RequestFriendship")
AnalyticsService:TrackEvent("Game", "RequestFriendship", "FriendPlayerPrompt")
local success = pcall(function()
LocalPlayer:RequestFriendship(playerToFriend)
end)
return success
end
function AtFriendLimit(player)
local friendCount = FriendingUtility:GetFriendCountAsync(player.UserId)
if friendCount == nil then
return false
end
if friendCount >= FriendingUtility:MaxFriendCount() then
return true
end
return false
end
function DoPromptRequestFriendPlayer(playerToFriend)
if LocalPlayer:IsFriendsWith(playerToFriend.UserId) then
return
end
local thumbnailUrl = BUST_THUMBNAIL_URL:format(BUST_THUMBNAIL_SIZE, BUST_THUMBNAIL_SIZE, playerToFriend.UserId)
local thumbnailUrlConsole = THUMBNAIL_URL:format(THUMBNAIL_SIZE, THUMBNAIL_SIZE, playerToFriend.UserId)
if GetFFlagUseThumbnailUrl() then
-- we use createFetchImageFunction which sets the image in the prompt, these are useless
thumbnailUrl = ""
thumbnailUrlConsole = ""
end
local function promptCompletedCallback(clickedConfirm)
if clickedConfirm then
if AtFriendLimit(LocalPlayer) then
while PromptCreator:IsCurrentlyPrompting() do
wait()
end
PromptCreator:CreatePrompt({
WindowTitle = "Friend Limit Reached",
MainText = "You can not send a friend request because you are at the max friend limit.",
ConfirmationText = "Okay",
CancelActive = false,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
StripeColor = Color3.fromRGB(183, 34, 54),
})
else
if AtFriendLimit(playerToFriend) then
local mainText = string.format("You can not send a friend request to %s because they are at the max friend limit.", playerToFriend.Name)
if FFlagFriendPlayerPromptUseFormatByKey then
mainText = RobloxTranslator:FormatByKey("FriendPlayerPrompt.promptCompletedCallback.AtFriendLimit", {RBX_NAME = playerToFriend.Name})
else
if FFlagUseNotificationsLocalization then
mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.promptCompletedCallback.AtFriendLimit",mainText),"{RBX_NAME}",playerToFriend.Name)
end
end
PromptCreator:CreatePrompt({
WindowTitle = "Error Sending Friend Request",
MainText = mainText,
ConfirmationText = "Okay",
CancelActive = false,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
StripeColor = Color3.fromRGB(183, 34, 54),
})
else
local successfullySentFriendRequest = SendFriendRequest(playerToFriend)
if not successfullySentFriendRequest then
while PromptCreator:IsCurrentlyPrompting() do
wait()
end
local mainText = string.format("An error occurred while sending %s a friend request. Please try again later.", playerToFriend.Name)
if FFlagFriendPlayerPromptUseFormatByKey then
mainText = RobloxTranslator:FormatByKey("FriendPlayerPrompt.promptCompletedCallback.UnknownError", {RBX_NAME = playerToFriend.Name})
else
if FFlagUseNotificationsLocalization then
mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.promptCompletedCallback.UnknownError",mainText),"{RBX_NAME}",playerToFriend.Name)
end
end
PromptCreator:CreatePrompt({
WindowTitle = "Error Sending Friend Request",
MainText = mainText,
ConfirmationText = "Okay",
CancelActive = false,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
StripeColor = Color3.fromRGB(183, 34, 54),
})
end
end
end
end
end
local mainText = string.format("Would you like to send %s a Friend Request?", playerToFriend.Name)
if FFlagFriendPlayerPromptUseFormatByKey then
mainText = RobloxTranslator:FormatByKey("FriendPlayerPrompt.DoPromptRequestFriendPlayer", {RBX_NAME = playerToFriend.Name})
else
if FFlagUseNotificationsLocalization then
mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.DoPromptRequestFriendPlayer",mainText),"{RBX_NAME}",playerToFriend.Name)
end
end
PromptCreator:CreatePrompt({
WindowTitle = "Send Friend Request?",
MainText = mainText,
ConfirmationText = "Send Request",
CancelText = "Cancel",
CancelActive = true,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToFriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToFriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
PromptCompletedCallback = promptCompletedCallback,
})
end
function PromptRequestFriendPlayer(player)
if LocalPlayer.UserId < 0 then
error("PromptSendFriendRequest can not be called for guests!")
end
if typeof(player) == "Instance" and player:IsA("Player") then
if player.UserId < 0 then
error("PromptSendFriendRequest can not be called on guests!")
end
if player == LocalPlayer then
error("PromptSendFriendRequest: A user can not friend themselves!")
end
DoPromptRequestFriendPlayer(player)
else
error("Invalid argument to PromptSendFriendRequest")
end
end
function UnFriendPlayer(playerToUnfriend)
local success = pcall(function()
LocalPlayer:RevokeFriendship(playerToUnfriend)
end)
return success
end
function DoPromptUnfriendPlayer(playerToUnfriend)
if not LocalPlayer:IsFriendsWith(playerToUnfriend.UserId) then
return
end
local thumbnailUrl = BUST_THUMBNAIL_URL:format(BUST_THUMBNAIL_SIZE, BUST_THUMBNAIL_SIZE, playerToUnfriend.UserId)
local thumbnailUrlConsole = THUMBNAIL_URL:format(THUMBNAIL_SIZE, THUMBNAIL_SIZE, playerToUnfriend.UserId)
if GetFFlagUseThumbnailUrl() then
-- we use createFetchImageFunction which sets the image in the prompt, these are useless
thumbnailUrl = ""
thumbnailUrlConsole = ""
end
local function promptCompletedCallback(clickedConfirm)
if clickedConfirm then
local successfullyUnfriended = UnFriendPlayer(playerToUnfriend)
if not successfullyUnfriended then
while PromptCreator:IsCurrentlyPrompting() do
wait()
end
local mainText = string.format("An error occurred while unfriending %s. Please try again later.", playerToUnfriend.Name)
if FFlagUseNotificationsLocalization then
mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.promptCompletedCallback.UnknownError",mainText),"{RBX_NAME}",playerToUnfriend.Name)
end
PromptCreator:CreatePrompt({
WindowTitle = "Error Unfriending Player",
MainText = mainText,
ConfirmationText = "Okay",
CancelActive = false,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToUnfriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnfriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
StripeColor = Color3.fromRGB(183, 34, 54),
})
end
end
end
local mainText = string.format("Would you like to remove %s from your friends list?", playerToUnfriend.Name)
if FFlagUseNotificationsLocalization then
mainText = string.gsub(LocalizedGetString("FriendPlayerPrompt.DoPromptUnfriendPlayer",mainText),"{RBX_NAME}",playerToUnfriend.Name)
end
PromptCreator:CreatePrompt({
WindowTitle = "Unfriend Player?",
MainText = mainText,
ConfirmationText = "Unfriend",
CancelText = "Cancel",
CancelActive = true,
Image = thumbnailUrl,
ImageConsoleVR = thumbnailUrlConsole,
FetchImageFunction = createFetchImageFunction(playerToUnfriend.UserId, REGULAR_THUMBNAIL_IMAGE_SIZE, REGULAR_THUMBNAIL_IMAGE_TYPE),
FetchImageFunctionConsoleVR = createFetchImageFunction(playerToUnfriend.UserId, CONSOLE_THUMBNAIL_IMAGE_SIZE, CONSOLE_THUMBNAIL_IMAGE_TYPE),
PromptCompletedCallback = promptCompletedCallback,
})
end
function PromptUnfriendPlayer(player)
if LocalPlayer.UserId < 0 then
error("PromptUnfriend can not be called for guests!")
end
if typeof(player) == "Instance" and player:IsA("Player") then
if player.UserId < 0 then
error("PromptUnfriend can not be called on guests!")
end
if player == LocalPlayer then
error("PromptUnfriend: A user can not unfriend themselves!")
end
DoPromptUnfriendPlayer(player)
else
error("Invalid argument to PromptUnfriend")
end
end
StarterGui:RegisterSetCore("PromptSendFriendRequest", PromptRequestFriendPlayer)
StarterGui:RegisterSetCore("PromptUnfriend", PromptUnfriendPlayer)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,198 @@
--[[
Entry point for the in-game chat from CoreScripts.
Currently, this only applies to the new version of BubbleChat, which is
written entirely in Roact. The chat list and legacy BubbleChat are still
PlayerScripts, and are both still accessible under
Modules/Server/ClientChat.
]]
local Chat = game:GetService("Chat")
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RobloxGui = CoreGui:WaitForChild("RobloxGui", math.huge)
local Roact = require(CorePackages.Packages.Roact)
local Rodux = require(CorePackages.Packages.Rodux)
local App = require(RobloxGui.Modules.InGameChat.BubbleChat.Components.App)
local chatReducer = require(RobloxGui.Modules.InGameChat.BubbleChat.Reducers.chatReducer)
local SetMessageText = require(RobloxGui.Modules.InGameChat.BubbleChat.Actions.SetMessageText)
local AddMessageFromEvent = require(RobloxGui.Modules.InGameChat.BubbleChat.Actions.AddMessageFromEvent)
local AddMessageWithTimeout = require(RobloxGui.Modules.InGameChat.BubbleChat.Actions.AddMessageWithTimeout)
local UpdateChatSettings = require(RobloxGui.Modules.InGameChat.BubbleChat.Actions.UpdateChatSettings)
local getPlayerFromPart = require(RobloxGui.Modules.InGameChat.BubbleChat.Helpers.getPlayerFromPart)
local validateMessage = require(RobloxGui.Modules.InGameChat.BubbleChat.Helpers.validateMessage)
local Constants = require(RobloxGui.Modules.InGameChat.BubbleChat.Constants)
local Types = require(RobloxGui.Modules.InGameChat.BubbleChat.Types)
local GameTranslator = require(RobloxGui.Modules.GameTranslator)
local MALFORMED_TEXT_WARNING = "Message text %q sent to chat event %q is not a valid UTF-8 characters sequence"
local WRONG_LENGTH_WARNING = "Message text %q is too long for chat event %q (expected a message of length %i, got %i)"
local MALFORMED_DATA_WARNING = "Malformed message data sent to chat event %q. If you have modified the chat system, " ..
"check what you are firing to this event"
local chatStore = Rodux.Store.new(chatReducer, nil, {
Rodux.thunkMiddleware,
})
local root = Roact.createElement(App, {
store = chatStore
})
local function validateMessageWithWarning(eventName, message)
local ok, length = validateMessage(message)
if not ok then
if length then
warn(WRONG_LENGTH_WARNING:format(message, eventName, Constants.MAX_MESSAGE_LENGTH, length))
else
warn(MALFORMED_TEXT_WARNING:format(message, eventName))
end
end
return ok
end
local function validateMessageData(eventName, messageData)
local ok, message = Types.IMessageData(messageData)
if not ok then
warn(MALFORMED_DATA_WARNING:format(eventName))
warn(message)
end
return ok
end
local handle, newMessageConn, messageDoneFilteringConn, chattedConn
local adorneeId = 0
local messageId = 0
local adorneeIdMap = {}
local function initBubbleChat()
handle = Roact.mount(root, CoreGui, "BubbleChat")
coroutine.resume(coroutine.create(function()
-- Using math.huge as the timeout means this will yield indefinitely without
-- logging a warning. We don't want to enforce that the
-- DefaultChatSystemChatEvents folder exists, but we do need to wait for it
-- incase it does. So this ensures the user can fork chat without getting a
-- warning they can't resolve.
local chatEvents = ReplicatedStorage:WaitForChild("DefaultChatSystemChatEvents", math.huge)
newMessageConn = chatEvents:WaitForChild("OnNewMessage", math.huge).OnClientEvent:Connect(function(messageData)
if not validateMessageData("OnNewMessage", messageData) then
return
end
if messageData.FromSpeaker == Players.LocalPlayer.Name then
if not validateMessageWithWarning("OnNewMessage", messageData.Message) then
return
end
chatStore:dispatch(AddMessageFromEvent(messageData))
end
end)
messageDoneFilteringConn = chatEvents:WaitForChild("OnMessageDoneFiltering", math.huge).OnClientEvent:Connect(function(messageData)
if not validateMessageData("OnMessageDoneFiltering", messageData)
or not validateMessageWithWarning("OnMessageDoneFiltering", messageData.Message) then
return
end
if messageData.FromSpeaker == Players.LocalPlayer.Name then
local id = tostring(messageData.ID)
chatStore:dispatch(SetMessageText(id, messageData.Message))
else
chatStore:dispatch(AddMessageFromEvent(messageData))
end
end)
end))
chattedConn = Chat.Chatted:Connect(function(partOrModel, message)
local part
if partOrModel:IsA("Model") then
if partOrModel.PrimaryPart then
part = partOrModel.PrimaryPart
else
part = partOrModel:FindFirstChildWhichIsA("BasePart", true)
end
else
part = partOrModel
end
local player
if part then
player = getPlayerFromPart(part)
end
local userId
if player then
userId = tostring(player.UserId)
else
local id = adorneeIdMap[partOrModel]
if id then
userId = id
else
adorneeId = adorneeId + 1
userId = "adornee_" .. adorneeId
adorneeIdMap[partOrModel] = userId
end
end
messageId = messageId + 1
local message = {
id = "chatted_" .. messageId,
userId = userId,
name = partOrModel.Name,
text = GameTranslator:TranslateGameText(CoreGui, message),
timestamp = os.time(),
adornee = partOrModel
}
chatStore:dispatch(AddMessageWithTimeout(message))
end)
end
local function destroyBubbleChat()
if handle then
Roact.unmount(handle)
handle = nil
end
if newMessageConn then
newMessageConn:Disconnect()
newMessageConn = nil
end
if messageDoneFilteringConn then
messageDoneFilteringConn:Disconnect()
messageDoneFilteringConn = nil
end
if chattedConn then
chattedConn:Disconnect()
chattedConn = nil
end
end
local function onBubbleChatEnabledChanged()
destroyBubbleChat()
if not game:GetEngineFeature("EnableBubbleChatFromChatService") or Chat.BubbleChatEnabled then
initBubbleChat()
end
end
if game:GetEngineFeature("EnableBubbleChatFromChatService") then
Chat:GetPropertyChangedSignal("BubbleChatEnabled"):Connect(onBubbleChatEnabledChanged)
end
onBubbleChatEnabledChanged()
if game:GetEngineFeature("BubbleChatSettingsApi") then
Chat.BubbleChatSettingsChanged:Connect(function(settings)
local ok, message = Types.IChatSettings(settings)
assert(ok, "Bad settings object passed to Chat:SetBubbleChatSettings:\n"..(message or ""))
chatStore:dispatch(UpdateChatSettings(settings))
end)
end
@@ -0,0 +1,96 @@
local CorePackages = game:GetService("CorePackages")
local GuiService = game:GetService("GuiService")
local CoreGuiService = game:GetService("CoreGui")
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local InspectAndBuyModules = CoreGuiModules:WaitForChild("InspectAndBuy")
local Roact = require(CorePackages.Roact)
local InspectAndBuy = require(InspectAndBuyModules.Components.InspectAndBuy)
local InspectAndBuyInstanceHandle = nil
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
local TopBar = require(RobloxGui.Modules.TopBar)
local FFlagInspectMenuDeveloperMethodsPolicy = game:DefineFastFlag("InspectMenuDeveloperMethodsPolicy", false)
local INSPECT_MENU_KEY = "InspectMenu"
local function mount(humanoidDescription, playerName, userId, ctx)
if InspectAndBuyInstanceHandle then
Roact.unmount(InspectAndBuyInstanceHandle)
InspectAndBuyInstanceHandle = nil
end
if InspectAndBuyInstanceHandle then
Roact.unmount(InspectAndBuyInstanceHandle)
InspectAndBuyInstanceHandle = nil
end
local inspectAndBuy = Roact.createElement(InspectAndBuy, {
humanoidDescription = humanoidDescription,
playerName = playerName,
playerId = userId,
ctx = ctx,
})
InspectAndBuyInstanceHandle = Roact.mount(inspectAndBuy, RobloxGui, "InspectAndBuy")
GuiService:SetMenuIsOpen(true, INSPECT_MENU_KEY)
TopBar:setInspectMenuOpen(true)
end
local function unmountInspectAndBuy()
if InspectAndBuyInstanceHandle then
Roact.unmount(InspectAndBuyInstanceHandle)
InspectAndBuyInstanceHandle = nil
GuiService:SetMenuIsOpen(false, INSPECT_MENU_KEY)
TopBar:setInspectMenuOpen(false)
end
end
local function mountInspectAndBuyFromHumanoidDescription(humanoidDescription, playerName, ctx)
mount(humanoidDescription, playerName, nil, ctx)
end
local function mountInspectAndBuyFromUserId(userId, ctx)
mount(nil, nil, userId, ctx)
end
GuiService.InspectPlayerFromHumanoidDescriptionRequest:Connect(function(humanoidDescription, playerName)
if FFlagInspectMenuDeveloperMethodsPolicy and PolicyService:IsSubjectToChinaPolicies() then
return
end
mountInspectAndBuyFromHumanoidDescription(humanoidDescription, playerName, "developerThroughHumanoidDescription")
end)
GuiService.InspectPlayerFromUserIdWithCtxRequest:Connect(function(userId, ctx)
if FFlagInspectMenuDeveloperMethodsPolicy and PolicyService:IsSubjectToChinaPolicies() then
return
end
mountInspectAndBuyFromUserId(userId, ctx)
end)
GuiService.CloseInspectMenuRequest:Connect(function()
if InspectAndBuyInstanceHandle then
unmountInspectAndBuy()
end
end)
GuiService.InspectMenuEnabledChangedSignal:Connect(function(enabled)
if not enabled and InspectAndBuyInstanceHandle then
unmountInspectAndBuy()
end
end)
if FFlagInspectMenuDeveloperMethodsPolicy then
spawn(function()
-- Check whether InspectMenu is disabled by policy after PolicyService is finished initializing
PolicyService:InitAsync()
if PolicyService:IsSubjectToChinaPolicies() and InspectAndBuyInstanceHandle then
unmountInspectAndBuy()
end
end)
end
@@ -0,0 +1,54 @@
local AnalyticsService = game:GetService("RbxAnalyticsService")
local SocialService = game:GetService("SocialService")
local CoreGui = game:GetService("CoreGui")
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local FFlagSafeGameInvite = game:DefineFastFlag("SafeGameInvite", false)
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Modules = RobloxGui.Modules
local SettingsHubDirectory = Modules.Settings
local ShareGameDirectory = SettingsHubDirectory.Pages.ShareGame
local Diag = require(CorePackages.AppTempCommon.AnalyticsReporters.Diag)
local EventStream = require(CorePackages.AppTempCommon.Temp.EventStream)
local InviteToGameAnalytics = require(ShareGameDirectory.Analytics.InviteToGameAnalytics)
local inviteToGameAnalytics = InviteToGameAnalytics.new()
:withEventStream(EventStream.new())
:withDiag(Diag.new(AnalyticsService))
:withButtonName(InviteToGameAnalytics.ButtonName.ModalPrompt)
local InviteToGamePrompt = require(ShareGameDirectory.InviteToGamePrompt)
local modalPrompt = InviteToGamePrompt.new(CoreGui)
:withSocialServiceAndLocalPlayer(SocialService, Players.LocalPlayer)
:withAnalytics(inviteToGameAnalytics)
local function canSendGameInviteAsync(player)
local success, result = pcall(function()
return SocialService:CanSendGameInviteAsync(player)
end)
return success and result
end
if FFlagSafeGameInvite then
SocialService.PromptInviteRequested:Connect(function(player)
if player ~= Players.LocalPlayer or not canSendGameInviteAsync(player) then
return
end
modalPrompt:show()
end)
else
SocialService.PromptInviteRequested:Connect(function(player)
if player ~= Players.LocalPlayer
or not SocialService:CanSendGameInviteAsync(player) then
return
end
modalPrompt:show()
end)
end
@@ -0,0 +1,777 @@
local PURPOSE_DATA = {
[Enum.DialogPurpose.Quest] = {
"rbxasset://textures/ui/dialog_purpose_quest.png",
Vector2.new(10, 34)
},
[Enum.DialogPurpose.Help] = {
"rbxasset://textures/ui/dialog_purpose_help.png",
Vector2.new(20, 35)
},
[Enum.DialogPurpose.Shop] = {
"rbxasset://textures/ui/dialog_purpose_shop.png",
Vector2.new(22, 43)
},
}
local TEXT_HEIGHT = 24 -- Pixel height of one row
local FONT_SIZE = Enum.FontSize.Size24
local BAR_THICKNESS = 6
local STYLE_PADDING = 17
local CHOICE_PADDING = 6 * 2 -- (Added to vertical height)
local PROMPT_SIZE = Vector2.new(80, 90)
local FRAME_WIDTH = 350
local WIDTH_BONUS = (STYLE_PADDING * 2) - BAR_THICKNESS
local XPOS_OFFSET = -(STYLE_PADDING - BAR_THICKNESS)
local playerService = game:GetService("Players")
local contextActionService = game:GetService("ContextActionService")
local guiService = game:GetService("GuiService")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local YPOS_OFFSET = -math.floor(STYLE_PADDING / 2)
local usingGamepad = false
local FlagHasReportedPlace = false
local localPlayer = playerService.LocalPlayer
while localPlayer == nil do
playerService.PlayerAdded:wait()
localPlayer = playerService.LocalPlayer
end
function setUsingGamepad(input, processed)
if input.UserInputType == Enum.UserInputType.Gamepad1 or input.UserInputType == Enum.UserInputType.Gamepad2 or
input.UserInputType == Enum.UserInputType.Gamepad3 or input.UserInputType == Enum.UserInputType.Gamepad4 then
usingGamepad = true
else
usingGamepad = false
end
end
game:GetService("UserInputService").InputBegan:connect(setUsingGamepad)
game:GetService("UserInputService").InputChanged:connect(setUsingGamepad)
function waitForProperty(instance, name)
while not instance[name] do
instance.Changed:wait()
end
end
local goodbyeChoiceActiveFlagSuccess, goodbyeChoiceActiveFlagValue = pcall(function()
return settings():GetFFlag("GoodbyeChoiceActiveProperty")
end)
local goodbyeChoiceActiveFlag = (goodbyeChoiceActiveFlagSuccess and goodbyeChoiceActiveFlagValue)
local mainFrame
local choices = {}
local lastChoice
local choiceMap = {}
local currentConversationDialog
local currentConversationPartner
local coroutineMap = {}
local currentDialogTimeoutCoroutine = nil
local tooFarAwayMessage = "You are too far away to chat!"
local tooFarAwaySize = 300
local characterWanderedOffMessage = "Chat ended because you walked away"
local characterWanderedOffSize = 350
local conversationTimedOut = "Chat ended because you didn't reply"
local conversationTimedOutSize = 350
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage')
local setDialogInUseEvent = RobloxReplicatedStorage:WaitForChild("SetDialogInUse", math.huge)
local FFlagFixDialogServerWait = require(RobloxGui.Modules.Common.Flags.GetFFlagFixDialogServerWait)()
local FFlagFixDialogScriptNilChecks = game:DefineFastFlag("FixDialogScriptNilChecks", false)
local chatNotificationGui
local messageDialog
local dialogMap = {}
local dialogConnections = {}
local touchControlGui = nil
local gui = nil
local isTenFootInterface = require(RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")):IsEnabled()
local utility = require(RobloxGui.Modules.Settings.Utility)
local GameTranslator = require(RobloxGui.Modules.GameTranslator)
local isSmallTouchScreen = utility:IsSmallTouchScreen()
if isTenFootInterface then
FONT_SIZE = Enum.FontSize.Size36
TEXT_HEIGHT = 36
FRAME_WIDTH = 500
elseif isSmallTouchScreen then
FONT_SIZE = Enum.FontSize.Size14
TEXT_HEIGHT = 14
FRAME_WIDTH = 250
end
if RobloxGui:FindFirstChild("ControlFrame") then
gui = RobloxGui.ControlFrame
else
gui = RobloxGui
end
local touchEnabled = game:GetService("UserInputService").TouchEnabled
local function isDialogMultiplePlayers(dialog)
local success, value = pcall(function() return dialog.BehaviorType == Enum.DialogBehaviorType.MultiplePlayers end)
return success and value or false
end
function currentTone()
if currentConversationDialog then
return currentConversationDialog.Tone
else
return Enum.DialogTone.Neutral
end
end
function createChatNotificationGui()
chatNotificationGui = Instance.new("BillboardGui")
chatNotificationGui.Name = "ChatNotificationGui"
chatNotificationGui.ExtentsOffset = Vector3.new(0, 1, 0)
chatNotificationGui.Size = UDim2.new(PROMPT_SIZE.X / 31.5, 0, PROMPT_SIZE.Y / 31.5, 0)
chatNotificationGui.SizeOffset = Vector2.new(0, 0)
chatNotificationGui.StudsOffset = Vector3.new(0, 3.7, 0)
chatNotificationGui.Enabled = true
chatNotificationGui.RobloxLocked = true
chatNotificationGui.Active = true
local button = Instance.new("ImageButton")
button.Name = "Background"
button.Active = false
button.BackgroundTransparency = 1
button.Position = UDim2.new(0, 0, 0, 0)
button.Size = UDim2.new(1, 0, 1, 0)
button.Image = ""
button.Parent = chatNotificationGui
local icon = Instance.new("ImageLabel")
icon.Name = "Icon"
icon.Position = UDim2.new(0, 0, 0, 0)
icon.Size = UDim2.new(1, 0, 1, 0)
icon.Image = ""
icon.BackgroundTransparency = 1
icon.Parent = button
local activationButton = Instance.new("ImageLabel")
activationButton.Name = "ActivationButton"
activationButton.Position = UDim2.new(-0.3, 0, -0.4, 0)
activationButton.Size = UDim2.new(.8, 0, .8 * (PROMPT_SIZE.X / PROMPT_SIZE.Y), 0)
activationButton.Image = "rbxasset://textures/ui/Settings/Help/XButtonDark.png"
activationButton.BackgroundTransparency = 1
activationButton.Visible = false
activationButton.Parent = button
end
function getChatColor(tone)
if tone == Enum.DialogTone.Neutral then
return Enum.ChatColor.Blue
elseif tone == Enum.DialogTone.Friendly then
return Enum.ChatColor.Green
elseif tone == Enum.DialogTone.Enemy then
return Enum.ChatColor.Red
end
end
function styleChoices()
for _, obj in pairs(choices) do
obj.BackgroundTransparency = 1
end
lastChoice.BackgroundTransparency = 1
end
function styleMainFrame(tone)
if tone == Enum.DialogTone.Neutral then
mainFrame.Style = Enum.FrameStyle.ChatBlue
elseif tone == Enum.DialogTone.Friendly then
mainFrame.Style = Enum.FrameStyle.ChatGreen
elseif tone == Enum.DialogTone.Enemy then
mainFrame.Style = Enum.FrameStyle.ChatRed
end
styleChoices()
end
function setChatNotificationTone(gui, purpose, tone)
if tone == Enum.DialogTone.Neutral then
gui.Background.Image = "rbxasset://textures/ui/chatBubble_blue_notify_bkg.png"
elseif tone == Enum.DialogTone.Friendly then
gui.Background.Image = "rbxasset://textures/ui/chatBubble_green_notify_bkg.png"
elseif tone == Enum.DialogTone.Enemy then
gui.Background.Image = "rbxasset://textures/ui/chatBubble_red_notify_bkg.png"
end
local newIcon, size = unpack(PURPOSE_DATA[purpose])
local relativeSize = size / PROMPT_SIZE
gui.Background.Icon.Size = UDim2.new(relativeSize.X, 0, relativeSize.Y, 0)
gui.Background.Icon.Position = UDim2.new(0.5 - (relativeSize.X / 2), 0, 0.4 - (relativeSize.Y / 2), 0)
gui.Background.Icon.Image = newIcon
end
function createMessageDialog()
messageDialog = Instance.new("Frame");
messageDialog.Name = "DialogScriptMessage"
messageDialog.Style = Enum.FrameStyle.Custom
messageDialog.BackgroundTransparency = 0.5
messageDialog.BackgroundColor3 = Color3.new(31 / 255, 31 / 255, 31 / 255)
messageDialog.Visible = false
messageDialog.RobloxLocked = true
local text = Instance.new("TextLabel")
text.Name = "Text"
text.Position = UDim2.new(0, 0, 0, -1)
text.Size = UDim2.new(1, 0, 1, 0)
text.FontSize = Enum.FontSize.Size14
text.BackgroundTransparency = 1
text.TextColor3 = Color3.new(1, 1, 1)
text.Parent = messageDialog
end
function showMessage(msg, size)
messageDialog.Text.Text = msg
messageDialog.Size = UDim2.new(0, size, 0, 40)
messageDialog.Position = UDim2.new(0.5, -size / 2, 0.5, -40)
messageDialog.Visible = true
wait(2)
messageDialog.Visible = false
end
function variableDelay(str)
local length = math.min(string.len(str), 100)
wait(0.75 + ((length / 75) * 1.5))
end
function resetColor(frame)
frame.BackgroundTransparency = 1
end
function wanderDialog()
mainFrame.Visible = false
endDialog()
showMessage(characterWanderedOffMessage, characterWanderedOffSize)
end
function timeoutDialog()
mainFrame.Visible = false
endDialog()
showMessage(conversationTimedOut, conversationTimedOutSize)
end
function normalEndDialog()
endDialog()
end
function endDialog()
if currentDialogTimeoutCoroutine then
coroutineMap[currentDialogTimeoutCoroutine] = false
currentDialogTimeoutCoroutine = nil
end
local dialog = currentConversationDialog
currentConversationDialog = nil
if dialog and dialog.InUse then
if FFlagFixDialogServerWait then
-- Waits 5 seconds before setting InUse to false
delay(5, function()
setDialogInUseEvent:FireServer(dialog, false)
dialog.InUse = false
end)
else
-- Waits 5 seconds before setting InUse to false
setDialogInUseEvent:FireServer(dialog, false, 5)
delay(5, function()
dialog.InUse = false
end)
end
end
for dialog, gui in pairs(dialogMap) do
if dialog and gui then
gui.Enabled = not dialog.InUse
end
end
contextActionService:UnbindCoreAction("Nothing")
currentConversationPartner = nil
if touchControlGui then
touchControlGui.Visible = true
end
end
function sanitizeMessage(msg)
if string.len(msg) == 0 then
return "..."
else
return msg
end
end
local function chatFunc(dialog, ...)
if isDialogMultiplePlayers(dialog) then
game:GetService("Chat"):ChatLocal(...)
else
game:GetService("Chat"):Chat(...)
end
end
function selectChoice(choice)
if FFlagFixDialogScriptNilChecks and not currentConversationDialog then
return
end
renewKillswitch(currentConversationDialog)
--First hide the Gui
mainFrame.Visible = false
if choice == lastChoice then
chatFunc(currentConversationDialog, localPlayer.Character, lastChoice.UserPrompt.Text, getChatColor(currentTone()))
normalEndDialog()
else
local dialogChoice = choiceMap[choice]
chatFunc(currentConversationDialog, localPlayer.Character, sanitizeMessage(dialogChoice.UserDialog), getChatColor(currentTone()))
wait(1)
if FFlagFixDialogScriptNilChecks and not currentConversationDialog then
return
end
currentConversationDialog:SignalDialogChoiceSelected(localPlayer, dialogChoice)
chatFunc(currentConversationDialog, currentConversationPartner, sanitizeMessage(dialogChoice.ResponseDialog), getChatColor(currentTone()))
variableDelay(dialogChoice.ResponseDialog)
presentDialogChoices(currentConversationPartner, dialogChoice:GetChildren(), dialogChoice)
end
end
function newChoice()
local dummyFrame = Instance.new("Frame")
dummyFrame.Visible = false
local frame = Instance.new("TextButton")
frame.BackgroundColor3 = Color3.new(227 / 255, 227 / 255, 227 / 255)
frame.BackgroundTransparency = 1
frame.AutoButtonColor = false
frame.BorderSizePixel = 0
frame.Text = ""
frame.MouseEnter:connect(function()
frame.BackgroundTransparency = 0
end)
frame.MouseLeave:connect(function()
frame.BackgroundTransparency = 1
end)
frame.SelectionImageObject = dummyFrame
frame.MouseButton1Click:connect(function()
selectChoice(frame)
end)
frame.RobloxLocked = true
local prompt = Instance.new("TextLabel")
prompt.Name = "UserPrompt"
prompt.BackgroundTransparency = 1
prompt.Font = Enum.Font.SourceSans
prompt.FontSize = FONT_SIZE
prompt.Position = UDim2.new(0, 40, 0, 0)
prompt.Size = UDim2.new(1, -32 - 40, 1, 0)
prompt.TextXAlignment = Enum.TextXAlignment.Left
prompt.TextYAlignment = Enum.TextYAlignment.Center
prompt.TextWrap = true
prompt.Parent = frame
local selectionButton = Instance.new("ImageLabel")
selectionButton.Name = "RBXchatDialogSelectionButton"
selectionButton.Position = UDim2.new(0, 0, 0.5, -33 / 2)
selectionButton.Size = UDim2.new(0, 33, 0, 33)
selectionButton.Image = "rbxasset://textures/ui/Settings/Help/AButtonLightSmall.png"
selectionButton.BackgroundTransparency = 1
selectionButton.Visible = false
selectionButton.Parent = frame
return frame
end
function initialize(parent)
choices[1] = newChoice()
choices[2] = newChoice()
choices[3] = newChoice()
choices[4] = newChoice()
lastChoice = newChoice()
lastChoice.UserPrompt.Text = "Goodbye!"
lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT + CHOICE_PADDING)
mainFrame = Instance.new("Frame")
mainFrame.Name = "UserDialogArea"
mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, 200)
mainFrame.Style = Enum.FrameStyle.ChatBlue
mainFrame.Visible = false
for n, obj in pairs(choices) do
obj.RobloxLocked = true
obj.Parent = mainFrame
end
lastChoice.RobloxLocked = true
lastChoice.Parent = mainFrame
mainFrame.RobloxLocked = true
mainFrame.Parent = parent
end
function presentDialogChoices(talkingPart, dialogChoices, parentDialog)
if not currentConversationDialog then
return
end
currentConversationPartner = talkingPart
local sortedDialogChoices = {}
for n, obj in pairs(dialogChoices) do
if obj:IsA("DialogChoice") then
table.insert(sortedDialogChoices, obj)
end
end
table.sort(sortedDialogChoices, function(a, b)
return a.Name < b.Name
end)
if #sortedDialogChoices == 0 then
normalEndDialog()
return
end
local pos = 1
local yPosition = 0
choiceMap = {}
for n, obj in pairs(choices) do
obj.Visible = false
end
for n, obj in pairs(sortedDialogChoices) do
if pos <= #choices then
--3 lines is the maximum, set it to that temporarily
choices[pos].Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT * 3)
GameTranslator:TranslateAndRegister(choices[pos].UserPrompt, obj, obj.UserDialog)
local height = (math.ceil(choices[pos].UserPrompt.TextBounds.Y / TEXT_HEIGHT) * TEXT_HEIGHT) + CHOICE_PADDING
choices[pos].Position = UDim2.new(0, XPOS_OFFSET, 0, YPOS_OFFSET + yPosition)
choices[pos].Size = UDim2.new(1, WIDTH_BONUS, 0, height)
choices[pos].Visible = true
choiceMap[choices[pos]] = obj
yPosition = yPosition + height + 1 -- The +1 makes highlights not overlap
pos = pos + 1
end
end
lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, TEXT_HEIGHT * 3)
lastChoice.UserPrompt.Text = parentDialog.GoodbyeDialog == "" and "Goodbye!" or parentDialog.GoodbyeDialog
local height = (math.ceil(lastChoice.UserPrompt.TextBounds.Y / TEXT_HEIGHT) * TEXT_HEIGHT) + CHOICE_PADDING
lastChoice.Size = UDim2.new(1, WIDTH_BONUS, 0, height)
lastChoice.Position = UDim2.new(0, XPOS_OFFSET, 0, YPOS_OFFSET + yPosition)
lastChoice.Visible = true
if goodbyeChoiceActiveFlag and not parentDialog.GoodbyeChoiceActive then
lastChoice.Visible = false
mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, yPosition + (STYLE_PADDING * 2) + (YPOS_OFFSET * 2))
else
mainFrame.Size = UDim2.new(0, FRAME_WIDTH, 0, yPosition + lastChoice.AbsoluteSize.Y + (STYLE_PADDING * 2) + (YPOS_OFFSET * 2))
end
mainFrame.Position = UDim2.new(0, 20, 1.0, -mainFrame.Size.Y.Offset - 20)
if isSmallTouchScreen then
local touchScreenGui = localPlayer.PlayerGui:FindFirstChild("TouchGui")
if touchScreenGui then
touchControlGui = touchScreenGui:FindFirstChild("TouchControlFrame")
if touchControlGui then
touchControlGui.Visible = false
end
end
mainFrame.Position = UDim2.new(0, 10, 1.0, -mainFrame.Size.Y.Offset)
end
styleMainFrame(currentTone())
mainFrame.Visible = true
if usingGamepad then
game:GetService("GuiService").SelectedCoreObject = choices[1]
end
end
function doDialog(dialog)
if dialog.InitialPrompt == "" then
warn("Can't start a dialog with an empty InitialPrompt")
return
end
local isMultiplePlayers = isDialogMultiplePlayers(dialog)
if dialog.InUse and not isMultiplePlayers then
return
else
currentConversationDialog = dialog
dialog.InUse = true
-- only bind if we actual enter the dialog
contextActionService:BindCoreAction("Nothing", function()
end, false, Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4)
-- Immediately sets InUse to true on the server
if FFlagFixDialogServerWait then
setDialogInUseEvent:FireServer(dialog, true)
else
setDialogInUseEvent:FireServer(dialog, true, 0)
end
end
chatFunc(dialog, dialog.Parent, dialog.InitialPrompt, getChatColor(dialog.Tone))
variableDelay(dialog.InitialPrompt)
presentDialogChoices(dialog.Parent, dialog:GetChildren(), dialog)
end
function renewKillswitch(dialog)
if currentDialogTimeoutCoroutine then
coroutineMap[currentDialogTimeoutCoroutine] = false
currentDialogTimeoutCoroutine = nil
end
currentDialogTimeoutCoroutine = coroutine.create(function(thisCoroutine)
wait(15)
if thisCoroutine ~= nil then
if coroutineMap[thisCoroutine] == nil then
if FFlagFixDialogServerWait then
setDialogInUseEvent:FireServer(dialog, false)
else
setDialogInUseEvent:FireServer(dialog, false, 0)
end
dialog.InUse = false
end
coroutineMap[thisCoroutine] = nil
end
end)
coroutine.resume(currentDialogTimeoutCoroutine, currentDialogTimeoutCoroutine)
end
function checkForLeaveArea()
while currentConversationDialog do
if currentConversationDialog.Parent and (localPlayer:DistanceFromCharacter(currentConversationDialog.Parent.Position) >= currentConversationDialog.ConversationDistance) then
wanderDialog()
end
wait(1)
end
end
function startDialog(dialog)
if dialog.Parent and dialog.Parent:IsA("BasePart") then
AnalyticsService:TrackEvent("Dialogue", "Old Dialogue", "Conversation Initiated")
if localPlayer:DistanceFromCharacter(dialog.Parent.Position) >= dialog.ConversationDistance then
showMessage(tooFarAwayMessage, tooFarAwaySize)
return
end
for dialog, gui in pairs(dialogMap) do
if dialog and gui then
gui.Enabled = false
end
end
renewKillswitch(dialog)
delay(1, checkForLeaveArea)
doDialog(dialog)
end
end
function removeDialog(dialog)
if dialogMap[dialog] then
dialogMap[dialog]:Destroy()
dialogMap[dialog] = nil
end
if dialogConnections[dialog] then
dialogConnections[dialog]:disconnect()
dialogConnections[dialog] = nil
end
end
function addDialog(dialog)
if dialog.Parent then
if dialog.Parent:IsA("BasePart") and dialog:IsDescendantOf(game.Workspace) then
FlagHasReportedPlace = true
AnalyticsService:TrackEvent("Dialogue", "Old Dialogue", "Used In Place", nil, game.PlaceId)
local chatGui = chatNotificationGui:clone()
chatGui.Adornee = dialog.Parent
chatGui.RobloxLocked = true
chatGui.Enabled = not dialog.InUse or isDialogMultiplePlayers(dialog)
chatGui.Parent = CoreGui
chatGui.Background.MouseButton1Click:connect(function()
startDialog(dialog)
end)
setChatNotificationTone(chatGui, dialog.Purpose, dialog.Tone)
dialogMap[dialog] = chatGui
dialogConnections[dialog] = dialog.Changed:connect(function(prop)
if prop == "Parent" and dialog.Parent then
--This handles the reparenting case, seperate from removal case
removeDialog(dialog)
addDialog(dialog)
elseif prop == "InUse" then
if not isDialogMultiplePlayers(dialog) then
chatGui.Enabled = (currentConversationDialog == nil) and not dialog.InUse
else
chatGui.Enabled = (currentConversationDialog ~= dialog)
end
if not dialog.InUse and not isDialogMultiplePlayers(dialog) and dialog == currentConversationDialog then
timeoutDialog()
end
elseif prop == "Tone" or prop == "Purpose" then
setChatNotificationTone(chatGui, dialog.Purpose, dialog.Tone)
end
end)
else -- still need to listen to parent changes even if current parent is not a BasePart
dialogConnections[dialog] = dialog.Changed:connect(function(prop)
if prop == "Parent" and dialog.Parent then
--This handles the reparenting case, seperate from removal case
removeDialog(dialog)
addDialog(dialog)
end
end)
end
end
end
function onLoad()
waitForProperty(localPlayer, "Character")
createChatNotificationGui()
createMessageDialog()
messageDialog.RobloxLocked = true
messageDialog.Parent = gui
gui:WaitForChild("BottomLeftControl")
local frame = Instance.new("Frame")
frame.Name = "DialogFrame"
frame.Position = UDim2.new(0, 0, 0, 0)
frame.Size = UDim2.new(0, 0, 0, 0)
frame.BackgroundTransparency = 1
frame.RobloxLocked = true
game:GetService("GuiService"):AddSelectionParent("RBXDialogGroup", frame)
if (touchEnabled and not isSmallTouchScreen) then
frame.Position = UDim2.new(0, 20, 0.5, 0)
frame.Size = UDim2.new(0.25, 0, 0.1, 0)
frame.Parent = gui
elseif isSmallTouchScreen then
frame.Position = UDim2.new(0, 0, .9, -10)
frame.Size = UDim2.new(0.25, 0, 0.1, 0)
frame.Parent = gui
else
frame.Parent = gui.BottomLeftControl
end
initialize(frame)
game:GetService("CollectionService").ItemAdded:connect(function(obj)
if obj:IsA("Dialog") then
addDialog(obj)
end
end)
game:GetService("CollectionService").ItemRemoved:connect(function(obj)
if obj:IsA("Dialog") then
removeDialog(obj)
end
end)
for i, obj in pairs(game:GetService("CollectionService"):GetCollection("Dialog")) do
if obj:IsA("Dialog") then
addDialog(obj)
end
end
end
function getLocalHumanoidRootPart()
if localPlayer.Character then
return localPlayer.Character:FindFirstChild("HumanoidRootPart")
end
end
function dialogIsValid(dialog)
return dialog and dialog.Parent and dialog.Parent:IsA("BasePart")
end
local lastClosestDialog = nil
local getClosestDialogToPosition = guiService.GetClosestDialogToPosition
game:GetService("RunService").Heartbeat:connect(function()
local closestDistance = math.huge
local closestDialog = nil
local humanoidRootPart = getLocalHumanoidRootPart()
if humanoidRootPart then
local characterPosition = humanoidRootPart.Position
closestDialog = getClosestDialogToPosition(guiService, characterPosition)
end
if getLocalHumanoidRootPart() and dialogIsValid(closestDialog) and currentConversationDialog == nil then
local dialogTriggerDistance = closestDialog.TriggerDistance
local dialogTriggerOffset = closestDialog.TriggerOffset
local distanceFromCharacterWithOffset = localPlayer:DistanceFromCharacter(
closestDialog.Parent.Position + dialogTriggerOffset
)
if dialogTriggerDistance ~= 0 and
distanceFromCharacterWithOffset < closestDialog.ConversationDistance and
distanceFromCharacterWithOffset < dialogTriggerDistance then
startDialog(closestDialog)
end
end
if usingGamepad == true then
if closestDialog ~= lastClosestDialog then
if dialogMap[lastClosestDialog] then
dialogMap[lastClosestDialog].Background.ActivationButton.Visible = false
end
lastClosestDialog = closestDialog
contextActionService:UnbindCoreAction("StartDialogAction")
if closestDialog ~= nil then
contextActionService:BindCoreAction("StartDialogAction", function(actionName, userInputState, inputObject)
if userInputState == Enum.UserInputState.Begin then
if closestDialog and closestDialog.Parent then
startDialog(closestDialog)
end
end
end, false, Enum.KeyCode.ButtonX)
if dialogMap[closestDialog] then
dialogMap[closestDialog].Background.ActivationButton.Visible = true
end
end -- closestDialog ~= nil
end -- closestDialog ~= lastClosestDialog
end -- usingGamepad == true
end)
local lastSelectedChoice = nil
guiService.Changed:connect(function(property)
if property == "SelectedCoreObject" then
if lastSelectedChoice and lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton") then
lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton").Visible = false
lastSelectedChoice.BackgroundTransparency = 1
end
lastSelectedChoice = guiService.SelectedCoreObject
if lastSelectedChoice and lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton") then
lastSelectedChoice:FindFirstChild("RBXchatDialogSelectionButton").Visible = true
lastSelectedChoice.BackgroundTransparency = 0
end
end
end)
onLoad()
@@ -0,0 +1,84 @@
--[[
// FileName: NetworkPause.lua
// Written by: woot3
// Description: Code for network pausing during streaming enabled.
]]
-- SERVICES
local PlayerService = game:GetService("Players")
local CoreGuiService = game:GetService("CoreGui")
local StarterGuiService = game:GetService("StarterGui")
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local RobloxGui = CoreGuiService:WaitForChild("RobloxGui")
local CoreGuiModules = RobloxGui:WaitForChild("Modules")
local Player = PlayerService.LocalPlayer
while not Player do
wait()
Player = PlayerService.LocalPlayer
end
-- MODULES
local NetworkPauseNotification = require(CoreGuiModules.NetworkPauseNotification)
local create = require(CoreGuiModules.Common.Create)
-- VARIABLES
local FFlagGameplayPausePausesInteraction = game:DefineFastFlag("GameplayPausePausesInteraction", false)
local isFirstPauseChange = true -- Skip showing UI on first pause to avoid displaying during loading process.
local Notification = NetworkPauseNotification.new()
-- container for the notification
local NetworkPauseContainer = FFlagGameplayPausePausesInteraction and create "Frame" {
Name = "Container",
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Active = false
}
local NetworkPauseGui = create "ScreenGui" {
Name = "RobloxNetworkPauseNotification",
OnTopOfCoreBlur = true,
DisplayOrder = 8,
Parent = CoreGuiService,
IgnoreGuiInset = FFlagGameplayPausePausesInteraction,
AutoLocalize = false,
NetworkPauseContainer
}
local function togglePauseState()
local paused = Player.GameplayPaused and NetworkPauseGui.Enabled and not isFirstPauseChange
isFirstPauseChange = false
if paused then
Notification:Show()
else
Notification:Hide()
end
if FFlagGameplayPausePausesInteraction then
NetworkPauseContainer.Active = paused
end
RunService:SetRobloxGuiFocused(paused)
end
Player:GetPropertyChangedSignal("GameplayPaused"):Connect(togglePauseState)
local function enableNotification(enabled)
assert(type(enabled) == "boolean", "Specified argument 'enabled' must be of type boolean")
if enabled == NetworkPauseGui.Enabled then return end
NetworkPauseGui.Enabled = enabled
togglePauseState()
end
if FFlagGameplayPausePausesInteraction then
Notification:SetParent(NetworkPauseContainer)
else
Notification:SetParent(NetworkPauseGui)
end
GuiService.NetworkPausedEnabledChanged:Connect(enableNotification)
@@ -0,0 +1,996 @@
--!nocheck
--[[
Filename: NotificationScript2.lua
Version 1.1
Written by: jmargh
Description: Handles notification gui for the following in game ROBLOX events
Badge Awarded
Player Points Awarded
Friend Request Recieved/New Friend
Graphics Quality Changed
Teleports
CreatePlaceInPlayerInventoryAsync
--]]
local BadgeService = game:GetService('BadgeService')
local GuiService = game:GetService('GuiService')
local Players = game:GetService('Players')
local PointsService = game:GetService('PointsService')
local MarketplaceService = game:GetService('MarketplaceService')
local TextService = game:GetService("TextService")
local HttpService = game:GetService("HttpService")
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local StarterGui = game:GetService("StarterGui")
local CoreGui = game:GetService("CoreGui")
local AnalyticsService = game:GetService("RbxAnalyticsService")
local VRService = game:GetService("VRService")
local GroupService = game:GetService("GroupService")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local success, result = pcall(function() return settings():GetFFlag('UseNotificationsLocalization') end)
local FFlagUseNotificationsLocalization = success and result
local FFlagNewAwardBadgeEndpoint = settings():GetFFlag('NewAwardBadgeEndpoint2')
local FFlagFixNotificationScriptError = game:DefineFastFlag("FixNotificationScriptError", false)
local GetFFlagRemoveInGameFollowingEvents = require(RobloxGui.Modules.Flags.GetFFlagRemoveInGameFollowingEvents)
local GetFixGraphicsQuality = require(RobloxGui.Modules.Flags.GetFixGraphicsQuality)
local isNewGamepadMenuEnabled = require(RobloxGui.Modules.Flags.isNewGamepadMenuEnabled)
local RobloxTranslator = require(RobloxGui:WaitForChild("Modules"):WaitForChild("RobloxTranslator"))
local function LocalizedGetString(key, rtv)
pcall(function()
rtv = RobloxTranslator:FormatByKey(key)
end)
return rtv
end
local LocalPlayer
while not Players.LocalPlayer do
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
end
LocalPlayer = Players.LocalPlayer
local RbxGui = script.Parent
local NotificationQueue = {}
local OverflowQueue = {}
local FriendRequestBlacklist = {}
local BadgeBlacklist = {}
local CurrentGraphicsQualityLevel = GameSettings.SavedQualityLevel.Value
local BindableEvent_SendNotificationInfo = Instance.new('BindableEvent')
BindableEvent_SendNotificationInfo.Name = "SendNotificationInfo"
BindableEvent_SendNotificationInfo.Parent = RobloxGui
local isPaused = false
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
local pointsNotificationsActive = true
local badgesNotificationsActive = true
local SocialUtil = require(RobloxGui.Modules:WaitForChild("SocialUtil"))
local GameTranslator = require(RobloxGui.Modules.GameTranslator)
local PolicyService = require(RobloxGui.Modules.Common.PolicyService)
local BG_TRANSPARENCY = 0.7
local MAX_NOTIFICATIONS = 3
local NOTIFICATION_Y_OFFSET = isTenFootInterface and 145 or 64
local NOTIFICATION_TITLE_Y_OFFSET = isTenFootInterface and 40 or 12
local NOTIFICATION_TEXT_Y_OFFSET = isTenFootInterface and -16 or 1
local NOTIFICATION_FRAME_WIDTH = isTenFootInterface and 450 or 200
local NOTIFICATION_TEXT_HEIGHT = isTenFootInterface and 85 or 28
local NOTIFICATION_TEXT_HEIGHT_MAX = isTenFootInterface and 170 or 56
local NOTIFICATION_TITLE_FONT_SIZE = isTenFootInterface and Enum.FontSize.Size42 or Enum.FontSize.Size18
local NOTIFICATION_TEXT_FONT_SIZE = isTenFootInterface and Enum.FontSize.Size36 or Enum.FontSize.Size14
local IMAGE_SIZE = isTenFootInterface and 72 or 48
local EASE_DIR = Enum.EasingDirection.InOut
local EASE_STYLE = Enum.EasingStyle.Sine
local TWEEN_TIME = 0.35
local DEFAULT_NOTIFICATION_DURATION = 5
local MAX_GET_FRIEND_IMAGE_YIELD_TIME = 5
local FRIEND_REQUEST_NOTIFICATION_THROTTLE = 5
local friendRequestNotificationFIntSuccess, friendRequestNotificationFIntValue = pcall(function() return tonumber(settings():GetFVariable("FriendRequestNotificationThrottle")) end)
if friendRequestNotificationFIntSuccess and friendRequestNotificationFIntValue ~= nil then
FRIEND_REQUEST_NOTIFICATION_THROTTLE = friendRequestNotificationFIntValue
end
StarterGui:RegisterSetCore(
"PointsNotificationsActive",
function(value) if type(value) == "boolean" then pointsNotificationsActive = value end end
)
StarterGui:RegisterSetCore(
"BadgesNotificationsActive",
function(value) if type(value) == "boolean" then badgesNotificationsActive = value end end
)
StarterGui:RegisterGetCore("PointsNotificationsActive", function() return pointsNotificationsActive end)
StarterGui:RegisterGetCore("BadgesNotificationsActive", function() return badgesNotificationsActive end)
local FFlagLocalizeVideoRecordAndScreenshotText = game:DefineFastFlag("LocalizeVideoRecordAndScreenshotText", false)
local PLAYER_POINTS_IMG = 'https://www.roblox.com/asset?id=206410433'
local BADGE_IMG = 'https://www.roblox.com/asset?id=206410289'
local function createFrame(name, size, position, bgt)
local frame = Instance.new('Frame')
frame.Name = name
frame.Size = size
frame.Position = position
frame.BackgroundTransparency = bgt
return frame
end
local function createTextButton(name, position)
local button = Instance.new('TextButton')
button.Name = name
button.Size = UDim2.new(0.5, -2, 0.5, 0)
button.Position = position
button.BackgroundTransparency = BG_TRANSPARENCY
button.BackgroundColor3 = Color3.new(0, 0, 0)
button.Font = Enum.Font.SourceSansBold
button.FontSize = Enum.FontSize.Size18
button.TextColor3 = Color3.new(1, 1, 1)
return button
end
local NotificationFrame = createFrame("NotificationFrame", UDim2.new(0, NOTIFICATION_FRAME_WIDTH, 0.42, 0), UDim2.new(1, -NOTIFICATION_FRAME_WIDTH-4, 0.50, 0), 1.0)
NotificationFrame.Parent = RbxGui
local DefaultNotification = createFrame("Notification", UDim2.new(1, 0, 0, NOTIFICATION_Y_OFFSET), UDim2.new(0, 0, 0, 0), BG_TRANSPARENCY)
DefaultNotification.BackgroundColor3 = Color3.new(0, 0, 0)
DefaultNotification.BorderSizePixel = 0
local NotificationTitle = Instance.new('TextLabel')
NotificationTitle.Name = "NotificationTitle"
NotificationTitle.Size = UDim2.new(0, 0, 0, 0)
NotificationTitle.Position = UDim2.new(0.5, 0, 0.5, -12)
NotificationTitle.BackgroundTransparency = 1
NotificationTitle.Font = Enum.Font.SourceSansBold
NotificationTitle.FontSize = NOTIFICATION_TITLE_FONT_SIZE
NotificationTitle.TextColor3 = Color3.new(0.97, 0.97, 0.97)
local NotificationText = Instance.new('TextLabel')
NotificationText.Name = "NotificationText"
NotificationText.Size = UDim2.new(1, -20, 0, 28)
NotificationText.Position = UDim2.new(0, 10, 0.5, 1)
NotificationText.BackgroundTransparency = 1
NotificationText.Font = Enum.Font.SourceSans
NotificationText.FontSize = NOTIFICATION_TEXT_FONT_SIZE
NotificationText.TextColor3 = Color3.new(0.92, 0.92, 0.92)
NotificationText.TextWrap = true
NotificationText.TextYAlignment = Enum.TextYAlignment.Top
local NotificationImage = Instance.new('ImageLabel')
NotificationImage.Name = "NotificationImage"
NotificationImage.Size = UDim2.new(0, IMAGE_SIZE, 0, IMAGE_SIZE)
NotificationImage.Position = UDim2.new(0, (1.0/6.0) * IMAGE_SIZE, 0, 0.5 * (NOTIFICATION_Y_OFFSET - IMAGE_SIZE))
NotificationImage.BackgroundTransparency = 1
NotificationImage.Image = ""
-- Would really like to get rid of this but some events still require this
local PopupFrame = createFrame("PopupFrame", UDim2.new(0, 360, 0, 160), UDim2.new(0.5, -180, 0.5, -50), 0)
PopupFrame.Style = Enum.FrameStyle.DropShadow
PopupFrame.ZIndex = 4
PopupFrame.Visible = false
PopupFrame.Parent = RbxGui
local PopupAcceptButton = Instance.new('TextButton')
PopupAcceptButton.Name = "PopupAcceptButton"
PopupAcceptButton.Size = UDim2.new(0, 100, 0, 50)
PopupAcceptButton.Position = UDim2.new(0.5, -102, 1, -58)
PopupAcceptButton.Style = Enum.ButtonStyle.RobloxRoundButton
PopupAcceptButton.Font = Enum.Font.SourceSansBold
PopupAcceptButton.FontSize = Enum.FontSize.Size24
PopupAcceptButton.TextColor3 = Color3.new(1, 1, 1)
PopupAcceptButton.Text = "Accept"
PopupAcceptButton.ZIndex = 5
PopupAcceptButton.Parent = PopupFrame
local PopupDeclineButton = PopupAcceptButton:Clone()
PopupDeclineButton.Name = "PopupDeclineButton"
PopupDeclineButton.Position = UDim2.new(0.5, 2, 1, -58)
PopupDeclineButton.Text = "Decline"
PopupDeclineButton.Parent = PopupFrame
local PopupOKButton = PopupAcceptButton:Clone()
PopupOKButton.Name = "PopupOKButton"
PopupOKButton.Position = UDim2.new(0.5, -50, 1, -58)
PopupOKButton.Text = "OK"
PopupOKButton.Visible = false
PopupOKButton.Parent = PopupFrame
local PopupText = Instance.new('TextLabel')
PopupText.Name = "PopupText"
PopupText.Size = UDim2.new(1, -16, 0.8, 0)
PopupText.Position = UDim2.new(0, 8, 0, 8)
PopupText.BackgroundTransparency = 1
PopupText.Font = Enum.Font.SourceSansBold
PopupText.FontSize = Enum.FontSize.Size36
PopupText.TextColor3 = Color3.new(0.97, 0.97, 0.97)
PopupText.TextWrap = true
PopupText.ZIndex = 5
PopupText.TextYAlignment = Enum.TextYAlignment.Top
PopupText.Text = "This is a popup"
PopupText.Parent = PopupFrame
local insertNotification
local removeNotification
local function getFriendImage(playerId)
-- SocialUtil.GetPlayerImage can yield for up to MAX_GET_FRIEND_IMAGE_YIELD_TIME seconds while waiting for thumbnail to be final.
-- It will just return an invalid thumbnail if a valid one can not be generated in time.
return SocialUtil.GetPlayerImage(playerId, Enum.ThumbnailSize.Size48x48, Enum.ThumbnailType.HeadShot, --[[timeOut = ]] MAX_GET_FRIEND_IMAGE_YIELD_TIME)
end
local function createNotification(title, text, image)
local notificationFrame = DefaultNotification:Clone()
notificationFrame.Position = UDim2.new(1, 4, 1, -NOTIFICATION_Y_OFFSET - 4)
local notificationTitle = NotificationTitle:Clone()
notificationTitle.Text = title
notificationTitle.Parent = notificationFrame
local notificationText = NotificationText:Clone()
notificationText.Text = text
notificationText.Parent = notificationFrame
if (image == nil or image == "") then
notificationFrame.Parent = NotificationFrame
if not notificationText.TextFits then
local textSize = TextService:GetTextSize(notificationText.Text, notificationText.TextSize, notificationText.Font, Vector2.new(notificationText.AbsoluteSize.X, 1000))
local addHeight = math.min(textSize.Y - notificationText.Size.Y.Offset, NOTIFICATION_TEXT_HEIGHT_MAX - notificationText.Size.Y.Offset)
notificationTitle.Position = notificationTitle.Position - UDim2.new(0, 0, 0, addHeight/2)
notificationText.Position = notificationText.Position - UDim2.new(0, 0, 0, addHeight/2)
notificationFrame.Size = notificationFrame.Size + UDim2.new(0, 0, 0, addHeight)
notificationText.Size = notificationText.Size + UDim2.new(0, 0, 0, addHeight)
end
notificationFrame.Parent = nil
end
if image and image ~= "" then
local notificationImage = NotificationImage:Clone()
notificationImage.Image = image
notificationImage.Parent = notificationFrame
notificationTitle.Position = UDim2.new(0, (4.0/3.0) * IMAGE_SIZE, 0.5, -NOTIFICATION_TITLE_Y_OFFSET)
notificationTitle.TextXAlignment = Enum.TextXAlignment.Left
notificationFrame.Parent = NotificationFrame
notificationText.Size = UDim2.new(1, -IMAGE_SIZE - 16, 0, NOTIFICATION_TEXT_HEIGHT)
notificationText.Position = UDim2.new(0, (4.0/3.0) * IMAGE_SIZE, 0.5, NOTIFICATION_TEXT_Y_OFFSET)
notificationText.TextXAlignment = Enum.TextXAlignment.Left
if not notificationText.TextFits then
local extraText = nil
local text = notificationText.Text
for i = string.len(text) - 1, 2, -1 do
if string.sub(text, i, i) == " " then
notificationText.Text = string.sub(text, 1, i - 1)
if notificationText.TextFits then
extraText = string.sub(text, i + 1)
break
end
end
end
if extraText then
local notificationText2 = NotificationText:Clone()
notificationText2.TextXAlignment = Enum.TextXAlignment.Left
notificationText2.Text = extraText
notificationText2.Name = "ExtraText"
notificationText2.Parent = notificationFrame
local textSize = TextService:GetTextSize(extraText, notificationText2.TextSize, notificationText2.Font, Vector2.new(notificationText2.AbsoluteSize.X, 1000))
local addHeight = math.min(textSize.Y, NOTIFICATION_TEXT_HEIGHT_MAX - notificationText.Size.Y.Offset)
notificationTitle.Position = notificationTitle.Position - UDim2.new(0, 0, 0, addHeight/2)
notificationText.Position = notificationText.Position - UDim2.new(0, 0, 0, addHeight/2)
notificationFrame.Size = notificationFrame.Size + UDim2.new(0, 0, 0, addHeight)
notificationText2.Size = UDim2.new(notificationText2.Size.X.Scale, notificationText2.Size.X.Offset, 0, addHeight)
notificationText2.AnchorPoint = Vector2.new(0.5, 0)
notificationText2.Position = UDim2.new(0.5, 0, notificationText.Position.Y.Scale, notificationText.Position.Y.Offset + notificationText.AbsoluteSize.Y)
else
notificationText.Text = text
end
end
notificationFrame.Parent = nil
end
GuiService:AddSelectionParent(HttpService:GenerateGUID(false), notificationFrame)
return notificationFrame
end
local function findNotification(notification)
local index = nil
for i = 1, #NotificationQueue do
if NotificationQueue[i] == notification then
return i
end
end
end
local function updateNotifications()
local pos = 1
local yOffset = 0
for i = #NotificationQueue, 1, -1 do
local currentNotification = NotificationQueue[i]
if currentNotification then
local frame = currentNotification.Frame
if frame and frame.Parent then
local thisOffset = currentNotification.IsFriend and (NOTIFICATION_Y_OFFSET + 2) * 1.5 or NOTIFICATION_Y_OFFSET
thisOffset = currentNotification.IsFriend and frame.Size.Y.Offset + ((NOTIFICATION_Y_OFFSET + 2) * 0.5) or frame.Size.Y.Offset
yOffset = yOffset + thisOffset
frame:TweenPosition(UDim2.new(0, 0, 1, -yOffset - (pos * 4)), EASE_DIR, EASE_STYLE, TWEEN_TIME, true,
function()
if currentNotification.TweenOutCallback then
currentNotification.TweenOutCallback()
end
end)
pos = pos + 1
end
end
end
end
local lastTimeInserted = 0
insertNotification = function(notification)
spawn(function()
while isPaused do wait() end
notification.IsActive = true
local size = #NotificationQueue
if size == MAX_NOTIFICATIONS then
NotificationQueue[1].Duration = 0
OverflowQueue[#OverflowQueue + 1] = notification
return
end
NotificationQueue[size + 1] = notification
notification.Frame.Parent = NotificationFrame
spawn(function()
wait(TWEEN_TIME * 2)
-- Wait for it to tween in, and then wait that same amount of time before calculating lifetime.
-- This is to have it not zoom out while half tweened in when the OverflowQueue forcibly
-- makes room for itself.
while(notification.Duration > 0) do
wait(0.2)
notification.Duration = notification.Duration - 0.2
end
removeNotification(notification)
end)
while tick() - lastTimeInserted < TWEEN_TIME do
wait()
end
lastTimeInserted = tick()
updateNotifications()
end)
end
removeNotification = function(notification)
if not notification then return end
--
local index = findNotification(notification)
table.remove(NotificationQueue, index)
local frame = notification.Frame
if frame and frame.Parent then
notification.IsActive = false
spawn(function()
while isPaused do wait() end
-- Tween out now, or set up to tween out immediately after current tween is finished, but don't interrupt.
local function doTweenOut()
if (not FFlagFixNotificationScriptError) or frame:IsDescendantOf(game) then
return frame:TweenPosition(
UDim2.new(1, 0, 1, frame.Position.Y.Offset), EASE_DIR, EASE_STYLE, TWEEN_TIME, false,
function()
frame:Destroy()
notification = nil
end
)
else
return false
end
end
if (not doTweenOut()) then
notification.TweenOutCallback = doTweenOut
end
end)
end
if #OverflowQueue > 0 then
local nextNotification = OverflowQueue[1]
table.remove(OverflowQueue, 1)
insertNotification(nextNotification)
if (#OverflowQueue > 0 and NotificationQueue[1]) then
NotificationQueue[1].Duration = 0
end
else
updateNotifications()
end
end
local function sendNotificationInfo(notificationInfo)
notificationInfo.Duration = notificationInfo.Duration or DEFAULT_NOTIFICATION_DURATION
BindableEvent_SendNotificationInfo:Fire(notificationInfo)
end
local function onSendNotificationInfo(notificationInfo)
if VRService.VREnabled then
--If VR is enabled, notifications will be handled by Modules.VR.NotificationHub
return
end
local callback = notificationInfo.Callback
local button1Text = notificationInfo.Button1Text
local button2Text = notificationInfo.Button2Text
local notification = {}
local notificationFrame
if notificationInfo.AutoLocalize then
-- AutoLocalize should only be used for Developer notifcations.
notificationFrame = createNotification(
GameTranslator:TranslateGameText(CoreGui, notificationInfo.Title),
GameTranslator:TranslateGameText(CoreGui, notificationInfo.Text),
notificationInfo.Image)
else
notificationFrame = createNotification(notificationInfo.Title, notificationInfo.Text, notificationInfo.Image)
end
local button1
if button1Text and button1Text ~= "" then
notification.IsFriend = true -- Prevents other notifications overlapping the buttons
button1 = createTextButton("Button1", UDim2.new(0, 0, 1, 2))
if notificationInfo.AutoLocalize then
GameTranslator:TranslateAndRegister(button1, CoreGui, button1Text)
else
button1.Text = button1Text
end
button1.Parent = notificationFrame
local button1ClickedConnection = nil
button1ClickedConnection = button1.MouseButton1Click:connect(function()
if button1ClickedConnection then
button1ClickedConnection:disconnect()
button1ClickedConnection = nil
removeNotification(notification)
if callback and type(callback) ~= "function" then -- callback should be a bindable
pcall(function() callback:Invoke(button1Text) end)
elseif type(callback) == "function" then
callback(button1Text)
end
end
end)
end
local button2
if button2Text and button2Text ~= "" then
notification.IsFriend = true
button2 = createTextButton("Button2", UDim2.new(0.5, 2, 1, 2))
if notificationInfo.AutoLocalize then
GameTranslator:TranslateAndRegister(button2, CoreGui, button2Text)
else
button2.Text = button2Text
end
button2.Parent = notificationFrame
local button2ClickedConnection = nil
button2ClickedConnection = button2.MouseButton1Click:connect(function()
if button2ClickedConnection then
button2ClickedConnection:disconnect()
button2ClickedConnection = nil
removeNotification(notification)
if callback and type(callback) ~= "function" then -- callback should be a bindable
pcall(function() callback:Invoke(button2Text) end)
elseif type(callback) == "function" then
callback(notificationInfo.Button2Text)
end
end
end)
else
-- Resize button1 to take up all the space under the notification if button2 doesn't exist
if button1 then
button1.Size = UDim2.new(1, -2, .5, 0)
end
end
notification.Frame = notificationFrame
notification.Duration = notificationInfo.Duration
insertNotification(notification)
end
BindableEvent_SendNotificationInfo.Event:connect(onSendNotificationInfo)
local function createDeveloperNotification(notificationTable)
if type(notificationTable) == "table" then
if type(notificationTable.Title) == "string" and type(notificationTable.Text) == "string" then
local iconImage = (type(notificationTable.Icon) == "string" and notificationTable.Icon or "")
local duration = (type(notificationTable.Duration) == "number" and notificationTable.Duration or DEFAULT_NOTIFICATION_DURATION)
local bindable = (typeof(notificationTable.Callback) == "Instance" and notificationTable.Callback:IsA("BindableFunction") and notificationTable.Callback or nil)
local button1Text = (type(notificationTable.Button1) == "string" and notificationTable.Button1 or "")
local button2Text = (type(notificationTable.Button2) == "string" and notificationTable.Button2 or "")
-- AutoLocalize allows developers to disable automatic localization if they have pre-localized it. Defaults true.
local autoLocalize = notificationTable.AutoLocalize == nil or notificationTable.AutoLocalize == true
local title = notificationTable.Title
local text = notificationTable.Text
sendNotificationInfo {
GroupName = "Developer",
Title = title,
Text = text,
Image = iconImage,
Duration = duration,
Callback = bindable,
Button1Text = button1Text,
Button2Text = button2Text,
AutoLocalize = autoLocalize,
}
end
end
end
StarterGui:RegisterSetCore("SendNotification", createDeveloperNotification)
-- New follower notification
spawn(function()
if isTenFootInterface or GetFFlagRemoveInGameFollowingEvents() then
--If on console, New follower notification should be blocked
return
end
local RobloxReplicatedStorage = game:GetService('RobloxReplicatedStorage')
local RemoteEvent_NewFollower = RobloxReplicatedStorage:WaitForChild('NewFollower', 86400) or RobloxReplicatedStorage:WaitForChild('NewFollower')
RemoteEvent_NewFollower.OnClientEvent:connect(function(followerRbxPlayer)
local message = RobloxTranslator:FormatByKey("NotificationScript2.NewFollower", {RBX_NAME = followerRbxPlayer.Name})
local image = getFriendImage(followerRbxPlayer.UserId)
sendNotificationInfo {
GroupName = "Friends",
Title = "New Follower",
Text = message,
DetailText = message,
Image = image,
Duration = 5
}
end)
end)
local checkFriendRequestIsThrottled; do
local friendRequestThrottlingMap = {}
checkFriendRequestIsThrottled = function(fromPlayer)
local throttleFinishedTime = friendRequestThrottlingMap[fromPlayer]
if throttleFinishedTime then
if tick() < throttleFinishedTime then
return true
end
end
friendRequestThrottlingMap[fromPlayer] = tick() + FRIEND_REQUEST_NOTIFICATION_THROTTLE
return false
end
end
local function sendFriendNotification(fromPlayer)
if checkFriendRequestIsThrottled(fromPlayer) then
return
end
local acceptText = "Accept"
local declineText = "Decline"
sendNotificationInfo {
GroupName = "Friends",
Title = fromPlayer.Name,
Text = "Sent you a friend request!",
DetailText = fromPlayer.Name,
Image = getFriendImage(fromPlayer.UserId),
Duration = 8,
Callback = function(buttonChosen)
if buttonChosen == acceptText then
AnalyticsService:ReportCounter("NotificationScript-RequestFriendship")
AnalyticsService:TrackEvent("Game", "RequestFriendship", "NotificationScript")
LocalPlayer:RequestFriendship(fromPlayer)
else
AnalyticsService:ReportCounter("NotificationScript-RevokeFriendship")
AnalyticsService:TrackEvent("Game", "RevokeFriendship", "NotificationScript")
LocalPlayer:RevokeFriendship(fromPlayer)
FriendRequestBlacklist[fromPlayer] = true
end
end,
Button1Text = acceptText,
Button2Text = declineText
}
end
local function onFriendRequestEvent(fromPlayer, toPlayer, event)
if fromPlayer ~= LocalPlayer and toPlayer ~= LocalPlayer then return end
--
if fromPlayer == LocalPlayer then
if event == Enum.FriendRequestEvent.Accept then
local detailText = RobloxTranslator:FormatByKey(
"NotificationScript2.FriendRequestEvent.Accept",
{RBX_NAME = toPlayer.Name})
sendNotificationInfo {
GroupName = "Friends",
Title = "New Friend",
Text = toPlayer.Name,
DetailText = detailText,
Image = getFriendImage(toPlayer.UserId),
Duration = DEFAULT_NOTIFICATION_DURATION
}
end
elseif toPlayer == LocalPlayer then
if event == Enum.FriendRequestEvent.Issue then
if FriendRequestBlacklist[fromPlayer] then return end
sendFriendNotification(fromPlayer)
elseif event == Enum.FriendRequestEvent.Accept then
local detailText = RobloxTranslator:FormatByKey("NotificationScript2.FriendRequestEvent.Accept", {RBX_NAME = fromPlayer.Name})
sendNotificationInfo {
GroupName = "Friends",
Title = "New Friend",
Text = fromPlayer.Name,
DetailText = detailText,
Image = getFriendImage(fromPlayer.UserId),
Duration = DEFAULT_NOTIFICATION_DURATION
}
end
end
end
local function onPointsAwarded(userId, pointsAwarded, userBalanceInGame, userTotalBalance)
if pointsNotificationsActive and userId == LocalPlayer.UserId then
local title, text, detailText
if pointsAwarded == 1 then
title = "Point Awarded"
text = RobloxTranslator:FormatByKey("NotificationScript2.onPointsAwarded.single", {RBX_NUMBER = tostring(pointsAwarded)})
elseif pointsAwarded > 0 then
title = "Points Awarded"
text = RobloxTranslator:FormatByKey("NotificationScript2.onPointsAwarded.multiple", {RBX_NUMBER = tostring(pointsAwarded)})
elseif pointsAwarded < 0 then
title = "Points Lost"
text = RobloxTranslator:FormatByKey("NotificationScript2.onPointsAwarded.negative", {RBX_NUMBER = tostring(pointsAwarded)})
else
--don't notify for 0 points, shouldn't even happen
return
end
detailText = text
sendNotificationInfo {
GroupName = "PlayerPoints",
Title = title,
Text = text,
DetailText = detailText,
Image = PLAYER_POINTS_IMG,
Duration = DEFAULT_NOTIFICATION_DURATION
}
end
end
--todo: rename to onBadgeAwarded when removing FFlagNewAwardBadgeEndpoint
local function onBadgeAwarded_NEW(userId, creatorId, badgeId)
if not BadgeBlacklist[badgeId] and badgesNotificationsActive and userId == LocalPlayer.UserId then
BadgeBlacklist[badgeId] = true
local creatorName = ""
if game.CreatorType == Enum.CreatorType.Group then
local groupInfo = GroupService:GetGroupInfoAsync(creatorId)
if groupInfo then
creatorName = groupInfo.Name
end
elseif game.CreatorType == Enum.CreatorType.User then
creatorName = Players:GetNameFromUserIdAsync(creatorId)
end
local badgeInfo = BadgeService:GetBadgeInfoAsync(badgeId)
local badgeAwardText = RobloxTranslator:FormatByKey("NotificationScript2.onBadgeAwardedDetail",
{RBX_NAME = LocalPlayer.Name, CREATOR_NAME = creatorName, BADGE_NAME = badgeInfo.DisplayName })
local badgeTitle = LocalizedGetString("NotificationScript2.onBadgeAwardedTitle")
sendNotificationInfo {
GroupName = "BadgeAwards",
Title = badgeTitle,
Text = badgeAwardText,
DetailText = badgeAwardText,
Image = BADGE_IMG,
Duration = DEFAULT_NOTIFICATION_DURATION
}
end
end
--todo: remove when removing FFlagNewAwardBadgeEndpoint
local function onBadgeAwarded(message, userId, badgeId)
if not BadgeBlacklist[badgeId] and badgesNotificationsActive and userId == LocalPlayer.UserId then
BadgeBlacklist[badgeId] = true
--SPTODO: Badge notifications are generated on the web and are not (for now) localized.
sendNotificationInfo {
GroupName = "BadgeAwards",
Title = "Badge Awarded",
Text = message,
DetailText = message,
Image = BADGE_IMG,
Duration = DEFAULT_NOTIFICATION_DURATION
}
end
end
-- DEPRECATED Remove with FixGraphicsQuality
function onGameSettingsChanged(property, amount)
if property == "SavedQualityLevel" then
local level = GameSettings.SavedQualityLevel.Value + amount
if level > 10 then
level = 10
elseif level < 1 then
level = 1
end
-- value of 0 is automatic, we do not want to send a notification in that case
if level > 0 and level ~= CurrentGraphicsQualityLevel and GameSettings.SavedQualityLevel ~= Enum.SavedQualitySetting.Automatic then
local action = (level > CurrentGraphicsQualityLevel) and "Increased" or "Decreased"
local message = ("%s to (%d)"):format(action, level)
if level > CurrentGraphicsQualityLevel then
message = RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Increased", {RBX_NUMBER = tostring(level)})
else
message = RobloxTranslator:FormatByKey("NotificationScrip2.onCurrentGraphicsQualityLevelChanged.Decreased", {RBX_NUMBER = tostring(level)})
end
sendNotificationInfo {
GroupName = "Graphics",
Title = "Graphics Quality",
Text = message,
DetailText = message,
Image = "",
Duration = 2
}
CurrentGraphicsQualityLevel = level
end
end
end
if not PolicyService:IsSubjectToChinaPolicies() then
if FFlagNewAwardBadgeEndpoint then
BadgeService.OnBadgeAwarded:connect(onBadgeAwarded_NEW)
BadgeService.BadgeAwarded:connect(onBadgeAwarded) -- todo: remove this when removing flag (only here in case old servers for a bit send down the old event)
else
BadgeService.BadgeAwarded:connect(onBadgeAwarded)
end
end
if not isTenFootInterface then
Players.FriendRequestEvent:connect(onFriendRequestEvent)
PointsService.PointsAwarded:connect(onPointsAwarded)
--GameSettings.Changed:connect(onGameSettingsChanged)
if not GetFixGraphicsQuality() then
game.GraphicsQualityChangeRequest:connect(function(graphicsIncrease) --graphicsIncrease is a boolean
onGameSettingsChanged("SavedQualityLevel", graphicsIncrease == true and 1 or -1)
end)
end
end
local allowScreenshots = not PolicyService:IsSubjectToChinaPolicies()
if allowScreenshots then
game.ScreenshotReady:Connect(function(path)
local titleText = "Screenshot Taken"
local descriptionText = "Check out your screenshots folder to see it."
local buttonText = "Open Folder"
if FFlagLocalizeVideoRecordAndScreenshotText then
titleText = RobloxTranslator:FormatByKey("NotificationScript2.Screenshot.Title")
descriptionText = RobloxTranslator:FormatByKey("NotificationScript2.Screenshot.Description")
buttonText = RobloxTranslator:FormatByKey("NotificationScript2.Screenshot.ButtonText")
end
sendNotificationInfo {
Title = titleText,
Text = descriptionText,
Duration = 3.0,
Button1Text = buttonText,
Callback = function(text)
if text == buttonText then
game:OpenScreenshotsFolder()
end
end
}
end)
settings():GetService("GameSettings").VideoRecordingChangeRequest:Connect(function(value)
if not value then
local titleText = "Video Recorded"
local descriptionText = "Check out your videos folder to see it."
local buttonText = "Open Folder"
if FFlagLocalizeVideoRecordAndScreenshotText then
titleText = RobloxTranslator:FormatByKey("NotificationScript2.Video.Title")
descriptionText = RobloxTranslator:FormatByKey("NotificationScript2.Video.Description")
buttonText = RobloxTranslator:FormatByKey("NotificationScript2.Video.ButtonText")
end
sendNotificationInfo {
Title = titleText,
Text = descriptionText,
Duration = 3.0,
Button1Text = buttonText,
Callback = function(text)
if text == buttonText then
game:OpenVideosFolder()
end
end
}
end
end)
end
GuiService.SendCoreUiNotification = function(title, text)
local notification = createNotification(title, text, "")
notification.BackgroundTransparency = .5
notification.Size = UDim2.new(.5, 0, .1, 0)
notification.Position = UDim2.new(.25, 0, -0.1, 0)
notification.NotificationTitle.FontSize = Enum.FontSize.Size36
notification.NotificationText.FontSize = Enum.FontSize.Size24
notification.Parent = RbxGui
notification:TweenPosition(UDim2.new(.25, 0, 0, 0), EASE_DIR, EASE_STYLE, TWEEN_TIME, true)
wait(5)
if notification then
notification:Destroy()
end
end
-- This is used for when a player calls CreatePlaceInPlayerInventoryAsync
local function onClientLuaDialogRequested(msg, accept, decline)
PopupText.Text = msg
--
local acceptCn, declineCn = nil, nil
local function disconnectCns()
if acceptCn then acceptCn:disconnect() end
if declineCn then declineCn:disconnect() end
--
GuiService:RemoveCenterDialog(PopupFrame)
PopupFrame.Visible = false
end
acceptCn = PopupAcceptButton.MouseButton1Click:connect(function()
disconnectCns()
MarketplaceService:SignalServerLuaDialogClosed(true)
end)
declineCn = PopupDeclineButton.MouseButton1Click:connect(function()
disconnectCns()
MarketplaceService:SignalServerLuaDialogClosed(false)
end)
local centerDialogSuccess = pcall(
function()
GuiService:AddCenterDialog(PopupFrame, Enum.CenterDialogType.QuitDialog,
function()
PopupOKButton.Visible = false
PopupAcceptButton.Visible = true
PopupDeclineButton.Visible = true
PopupAcceptButton.Text = accept
PopupDeclineButton.Text = decline
PopupFrame.Visible = true
end,
function()
PopupFrame.Visible = false
end)
end)
if not centerDialogSuccess then
PopupFrame.Visible = true
PopupAcceptButton.Text = accept
PopupDeclineButton.Text = decline
end
return true
end
MarketplaceService.ClientLuaDialogRequested:connect(onClientLuaDialogRequested)
if not isTenFootInterface and not isNewGamepadMenuEnabled() then
local gamepadMenu = RobloxGui:WaitForChild("CoreScripts/GamepadMenu")
local gamepadNotifications = gamepadMenu:FindFirstChild("GamepadNotifications")
while not gamepadNotifications do
wait()
gamepadNotifications = gamepadMenu:FindFirstChild("GamepadNotifications")
end
local leaveNotificationFunc = function(name, state, inputObject)
if state ~= Enum.UserInputState.Begin then return end
if GuiService.SelectedCoreObject:IsDescendantOf(NotificationFrame) then
GuiService.SelectedCoreObject = nil
end
ContextActionService:UnbindCoreAction("LeaveNotificationSelection")
end
gamepadNotifications.Event:connect(function(isSelected)
if not isSelected then return end
isPaused = true
local notifications = NotificationFrame:GetChildren()
for i = 1, #notifications do
local noteComponents = notifications[i]:GetChildren()
for j = 1, #noteComponents do
if noteComponents[j]:IsA("GuiButton") and noteComponents[j].Visible then
GuiService.SelectedCoreObject = noteComponents[j]
break
end
end
end
if GuiService.SelectedCoreObject then
ContextActionService:BindCoreAction("LeaveNotificationSelection", leaveNotificationFunc, false, Enum.KeyCode.ButtonB)
else
isPaused = false
local utility = require(RobloxGui.Modules.Settings.Utility)
local okPressedFunc = function() end
utility:ShowAlert("You have no notifications", "Ok", --[[settingsHub]] nil, okPressedFunc, true)
end
end)
GuiService.Changed:connect(function(prop)
if prop == "SelectedCoreObject" then
if not GuiService.SelectedCoreObject or not GuiService.SelectedCoreObject:IsDescendantOf(NotificationFrame) then
isPaused = false
end
end
end)
end
local Platform = UserInputService:GetPlatform()
local Modules = RobloxGui:FindFirstChild('Modules')
local CSMModule = Modules:FindFirstChild('ControllerStateManager')
if Modules and not CSMModule then
local ShellModules = Modules:FindFirstChild('Shell')
if ShellModules then
CSMModule = ShellModules:FindFirstChild('ControllerStateManager')
end
end
if Platform == Enum.Platform.XBoxOne then
-- Platform hook for controller connection events
-- Displays overlay to user on controller connection lost
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
if PlatformService and CSMModule then
local controllerStateManager = require(CSMModule)
if controllerStateManager then
controllerStateManager:Initialize()
if not game:IsLoaded() then
game.Loaded:wait()
end
-- retro check in case of controller disconnect while loading
-- for now, gamepad1 is always mapped to the active user
controllerStateManager:CheckUserConnected()
end
end
end
@@ -0,0 +1,227 @@
--[[
Filename: PerformanceStatsManagerScript.lua
Written by: dbanks
Description: Handles performance stats gui.
--]]
--[[ Services ]]--
local PlayersService = game:GetService("Players")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local CoreGuiService = game:GetService('CoreGui')
local AnalyticsService = game:GetService("RbxAnalyticsService")
--[[ Modules ]]--
local GoogleAnalyticsUtils = require(CoreGuiService.RobloxGui.Modules.GoogleAnalyticsUtils)
local StatsAggregatorManagerClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsAggregatorManager)
local StatsButtonClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsButton)
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local StatsViewerClass = require(CoreGuiService.RobloxGui.Modules.Stats.StatsViewer)
--[[ Script Variables ]]--
local masterFrame = Instance.new("Frame")
masterFrame.Name = "PerformanceStats"
local FFlagPerformanceProfilerAnalytics = settings():GetFFlag("PerformanceProfilerAnalytics")
local statsAggregatorManager = StatsAggregatorManagerClass.getSingleton()
local statsViewer = StatsViewerClass.new()
local statsButtonsByType ={}
local currentStatType = nil
local OpenCounterName = "OpenPerformanceProfiler"
local TimeOpenCounterName = "TimeOpenPerformanceProfiler"
local openTimeStamp = nil
for i, statType in ipairs(StatsUtils.AllStatTypes) do
local button = StatsButtonClass.new(statType)
statsButtonsByType[statType] = button
end
function ShowMasterFrame()
masterFrame.Visible = true
masterFrame.Parent = CoreGuiService.RobloxGui
end
function HideMasterFrame()
masterFrame.Visible = false
masterFrame.Parent = nil
end
--[[ Functions ]]--
function ConfigureMasterFrame()
-- Set up the main frame that contains the whole PS GUI.
-- Avoid the top button bar.
masterFrame.Position = UDim2.new(0, 0, 0, 0)
masterFrame.Size = UDim2.new(1, 0, 1, 0)
masterFrame.Selectable = false
masterFrame.BackgroundTransparency = 1.0
masterFrame.Active = false
masterFrame.ZIndex = 0
HideMasterFrame()
-- FIXME(dbanks)
-- Debug, can see the whole frame.
-- masterFrame.BackgroundColor3 = Color3.new(0, 0.5, 0.5)
-- masterFrame.BackgroundTransparency = 0.8
end
function ConfigureStatButtonsInMasterFrame()
-- Set up the row of buttons across the top and handler for button press.
for i, statType in ipairs(StatsUtils.AllStatTypes) do
AddButton(statType, i)
end
end
function OnButtonToggled(toggledStatType)
local toggledButton = statsButtonsByType[toggledStatType]
local selectedState = toggledButton._isSelected
selectedState = not selectedState
if (selectedState) then
currentStatType = toggledStatType
else
currentStatType = nil
end
UpdateButtonSelectedStates()
UpdateViewerVisibility()
end
function UpdateButtonSelectedStates()
for i, buttonType in ipairs(StatsUtils.AllStatTypes) do
local button = statsButtonsByType[buttonType]
button:SetIsSelected(buttonType == currentStatType)
end
end
function UpdateViewerVisibility()
-- If a particular button/tab is on, show the Viewer.
--
-- Don't bother if we're already there.
if (currentStatType == nil) then
if statsViewer:GetIsVisible() then
statsViewer:SetVisible(false)
statsViewer:SetStatsAggregator(nil)
end
else
local somethingChanged = false
if not statsViewer:GetIsVisible() then
somethingChanged = true
statsViewer:SetVisible(true)
end
if currentStatType ~= statsViewer:GetStatType() then
statsViewer:SetStatType(currentStatType)
statsViewer:SetStatsAggregator(statsAggregatorManager:GetAggregator(currentStatType))
somethingChanged = true
end
if somethingChanged then
-- track it.
AnalyticsService:TrackEvent("Game", "Enlarge PerfStat", currentStatType, 0)
end
end
end
function AddButton(statType, index)
-- Configure size and position of button.
-- Configure callback behavior to toggle
-- button on or off and show/hide viewer.
-- Parent button in main screen.
local button = statsButtonsByType[statType]
button:SetParent(masterFrame)
button:SetStatsAggregator(
statsAggregatorManager:GetAggregator(statType))
local fraction = 1.0/StatsUtils.NumButtonTypes
local size = UDim2.new(fraction, 0, 0, StatsUtils.ButtonHeight)
local position = UDim2.new(fraction * (index - 1), 0, 0, 0)
button:SetSizeAndPosition(size, position)
button:SetToggleCallbackFunction(OnButtonToggled)
end
function ConfigureStatViewerInMasterFrame()
-- Set up the widget that shows currently selected button.
statsViewer:SetParent(masterFrame)
local size = UDim2.new(0, StatsUtils.ViewerWidth, 0, StatsUtils.ViewerHeight)
local position = UDim2.new(1, -StatsUtils.ViewerWidth,
0, StatsUtils.ButtonHeight + StatsUtils.ViewerTopMargin)
statsViewer:SetSizeAndPosition(size, position)
end
function UpdatePerformanceStatsVisibility()
local shouldBeVisible = StatsUtils.PerformanceStatsShouldBeVisible()
if (shouldBeVisible == masterFrame.Visible) then
return
end
if shouldBeVisible then
ShowMasterFrame()
else
HideMasterFrame()
end
-- Let the children respond to the transition that they are/are not visible.
statsViewer:OnPerformanceStatsShouldBeVisibleChanged()
for i, buttonType in ipairs(StatsUtils.AllStatTypes) do
local button = statsButtonsByType[buttonType]
button:OnPerformanceStatsShouldBeVisibleChanged()
end
-- track it.
local actionName = "Hide PerfStats"
if shouldBeVisible then
actionName = "Show PerfStats"
end
AnalyticsService:TrackEvent("Game", actionName, "", 0)
if FFlagPerformanceProfilerAnalytics then
if shouldBeVisible then
openTimeStamp = time()
AnalyticsService:ReportCounter(OpenCounterName, 1)
else
if openTimeStamp then
local timeDiff = time() - openTimeStamp
AnalyticsService:ReportStats(TimeOpenCounterName, timeDiff)
end
end
end
end
--[[ Top Level Code ]]--
-- Set up our GUI.
ConfigureMasterFrame()
ConfigureStatButtonsInMasterFrame()
ConfigureStatViewerInMasterFrame()
-- Watch for changes in performance stats visibility.
GameSettings.PerformanceStatsVisibleChanged:connect(
UpdatePerformanceStatsVisibility)
-- Make sure we're showing buttons and viewer based on current selection.
UpdateButtonSelectedStates()
UpdateViewerVisibility()
-- Make sure stats are visible or not, as specified by current setting.
UpdatePerformanceStatsVisibility()
-- This may change if Player shows up...
spawn(function()
local localPlayer = PlayersService.LocalPlayer
while not localPlayer do
PlayersService.PlayerAdded:wait()
localPlayer = PlayersService.LocalPlayer
end
UpdatePerformanceStatsVisibility()
end)
@@ -0,0 +1,105 @@
local Players = game:GetService("Players")
local CoreGuiService = game:GetService("CoreGui")
local RobloxReplicatedStorage = game:GetService("RobloxReplicatedStorage")
local RobloxGui = CoreGuiService:FindFirstChild("RobloxGui")
local CoreGuiModules = RobloxGui:FindFirstChild("Modules")
local CommonModules = CoreGuiModules:FindFirstChild("Common")
local Rigging = require(CommonModules:FindFirstChild("RagdollRigging"))
local HumanoidReadyUtil = require(CommonModules:FindFirstChild("HumanoidReadyUtil"))
local localPlayer = Players.LocalPlayer
if not localPlayer then
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
localPlayer = Players.LocalPlayer
end
-- If we never get this I don't care. If it doesen't exist I was going to return immediately anyway.
local DeathTypeValue = RobloxReplicatedStorage:WaitForChild("DeathType", math.huge)
if not DeathTypeValue or DeathTypeValue.Value ~= "Ragdoll" then
return
end
local remote = RobloxReplicatedStorage:WaitForChild("OnRagdoll", math.huge)
-- If we're missing our RemoteEvent to notify the server that we've started simulating our
-- ragdoll so it can authoritatively replicate the joint removal, don't ragdoll at all.
if not (remote and remote:IsA("RemoteEvent")) then
return
end
local function onOwnedHumanoidDeath(character, humanoid)
-- We first disable the motors on the network owner (the player that owns this character).
--
-- This way there is no visible round trip hitch. By the time the server receives the joint
-- break physics data for the child parts should already be available. Seamless transition.
--
-- If we initiated ragdoll by disabling joints on the server there's a visible hitch while the
-- server waits at least a full round trip time for the network owner to receive the joint
-- removal, start simulating the ragdoll, and replicate physics data. Meanwhile the other body
-- parts would be frozen in air on the server and other clients until physics data arives from
-- the owner. The ragdolled player wouldn't see it, but other players would.
--
-- We also specifically do not disable the root joint on the client so we can maintain a
-- consistent mechanism and network ownership unit root. If we did disable the root joint we'd
-- be creating a new, seperate network ownership unit that we would have to wait for the server
-- to assign us network ownership of before we would start simulating and replicating physics
-- data for it, creating an additional round trip hitch on our end for our own character.
local motors = Rigging.disableMotors(character, humanoid.RigType)
-- Apply velocities from animation to the child parts to mantain visual momentum.
--
-- This should be done on the network owner's side just after disabling the kinematic joint so
-- the child parts are split off as seperate dynamic bodies. For consistent animation times and
-- visual momentum we want to do this on the machine that controls animation state for the
-- character and will be simulating the ragdoll, in this case the client.
--
-- It's also important that this is called *before* any animations are canceled or changed after
-- death! Otherwise there will be no animations to get velocities from or the velocities won't
-- be consistent!
local animator = humanoid:FindFirstChildWhichIsA("Animator")
if animator then
animator:ApplyJointVelocities(motors)
end
-- Tell the server that we started simulating our ragdoll
remote:FireServer(humanoid)
-- stiff shock phase...
wait(0.1)
-- gradually give up...
Rigging.easeJointFriction(character, 0.85)
end
HumanoidReadyUtil.registerHumanoidReady(function(player, character, humanoid)
local ancestryChangedConn
local diedConn
local function disconnect()
ancestryChangedConn:Disconnect()
diedConn:Disconnect()
end
-- Handle Humanoid death
diedConn = humanoid.Died:Connect(function()
-- Assume death is final
disconnect()
-- Any character: handle fade out on death
delay(2.0, function()
-- fade into the mist...
Rigging.disableParticleEmittersAndFadeOut(character, 0.4)
end)
-- Just my character: initiate ragdoll and do friction easing
if player == localPlayer then
onOwnedHumanoidDeath(character, humanoid)
end
end)
-- Handle connection cleanup on remove
ancestryChangedConn = humanoid.AncestryChanged:Connect(function(_child, parent)
if not game:IsAncestorOf(parent) then
disconnect()
end
end)
end)
@@ -0,0 +1,884 @@
local UserInputService = game:GetService("UserInputService")
local ProximityPromptService = game:GetService("ProximityPromptService")
local CoreGui = game:GetService("CoreGui")
local TweenService = game:GetService("TweenService")
local TextService = game:GetService("TextService")
local Players = game:GetService("Players")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local FFlagProximityPromptLocalization = game:DefineFastFlag("ProximityPromptLocalization", false)
local FFlagProximityPromptNoButtonDrag = game:DefineFastFlag("ProximityPromptNoButtonDrag", false)
local FFlagProximityPromptLiveChanges = game:DefineFastFlag("ProximityPromptLiveChanges", false)
local FFlagProximityPromptMoreKeyCodes = game:DefineFastFlag("ProximityPromptMoreKeyCodes", false)
local FFlagProximityPromptsFadeIn = game:DefineFastFlag("ProximityPromptsFadeIn", false)
local FFlagProximityPromptMoreKeyCodes2 = game:DefineFastFlag("ProximityPromptMoreKeyCodes2", false)
local PlayerGui
if FFlagProximityPromptLocalization then
local LocalPlayer = Players.LocalPlayer
while LocalPlayer == nil do
Players.ChildAdded:wait()
LocalPlayer = Players.LocalPlayer
end
PlayerGui = LocalPlayer:WaitForChild("PlayerGui")
end
local GamepadButtonImage = {
[Enum.KeyCode.ButtonX] = "rbxasset://textures/ui/Controls/xboxX.png",
[Enum.KeyCode.ButtonY] = "rbxasset://textures/ui/Controls/xboxY.png",
[Enum.KeyCode.ButtonA] = "rbxasset://textures/ui/Controls/xboxA.png",
[Enum.KeyCode.ButtonB] = "rbxasset://textures/ui/Controls/xboxB.png",
[Enum.KeyCode.DPadLeft] = "rbxasset://textures/ui/Controls/dpadLeft.png",
[Enum.KeyCode.DPadRight] = "rbxasset://textures/ui/Controls/dpadRight.png",
[Enum.KeyCode.DPadUp] = "rbxasset://textures/ui/Controls/dpadUp.png",
[Enum.KeyCode.DPadDown] = "rbxasset://textures/ui/Controls/dpadDown.png",
[Enum.KeyCode.ButtonSelect] = "rbxasset://textures/ui/Controls/xboxmenu.png",
[Enum.KeyCode.ButtonL1] = "rbxasset://textures/ui/Controls/xboxLS.png",
[Enum.KeyCode.ButtonR1] = "rbxasset://textures/ui/Controls/xboxRS.png",
}
local KeyboardButtonImage = {
[Enum.KeyCode.Backspace] = "rbxasset://textures/ui/Controls/backspace.png",
[Enum.KeyCode.Return] = "rbxasset://textures/ui/Controls/return.png",
[Enum.KeyCode.LeftShift] = "rbxasset://textures/ui/Controls/shift.png",
[Enum.KeyCode.RightShift] = "rbxasset://textures/ui/Controls/shift.png",
[Enum.KeyCode.Tab] = "rbxasset://textures/ui/Controls/tab.png",
}
-- This is only available in Core Scripts, so this block must be removed
-- when copying this code as an example for customization in developer scripts
if FFlagProximityPromptMoreKeyCodes2 then
if UserInputService:GetPlatform() == Enum.Platform.OSX then
KeyboardButtonImage[Enum.KeyCode.LeftMeta] = "rbxasset://textures/ui/Controls/command.png"
KeyboardButtonImage[Enum.KeyCode.RightMeta] = "rbxasset://textures/ui/Controls/command.png"
KeyboardButtonImage[Enum.KeyCode.LeftAlt] = "rbxasset://textures/ui/Controls/option.png"
KeyboardButtonImage[Enum.KeyCode.RightAlt] = "rbxasset://textures/ui/Controls/option.png"
end
else
if UserInputService:GetPlatform() == Enum.Platform.OSX then
KeyboardButtonImage[Enum.KeyCode.LeftControl] = "rbxasset://textures/ui/Controls/command.png"
KeyboardButtonImage[Enum.KeyCode.RightControl] = "rbxasset://textures/ui/Controls/command.png"
KeyboardButtonImage[Enum.KeyCode.LeftAlt] = "rbxasset://textures/ui/Controls/option.png"
KeyboardButtonImage[Enum.KeyCode.RightAlt] = "rbxasset://textures/ui/Controls/option.png"
end
end
local KeyboardButtonIconMapping = {
["'"] = "rbxasset://textures/ui/Controls/apostrophe.png",
[","] = "rbxasset://textures/ui/Controls/comma.png",
["`"] = "rbxasset://textures/ui/Controls/graveaccent.png",
["."] = "rbxasset://textures/ui/Controls/period.png",
[" "] = "rbxasset://textures/ui/Controls/spacebar.png",
}
local KeyCodeToTextMapping = {
[Enum.KeyCode.LeftControl] = "Ctrl",
[Enum.KeyCode.RightControl] = "Ctrl",
[Enum.KeyCode.LeftAlt] = "Alt",
[Enum.KeyCode.RightAlt] = "Alt",
[Enum.KeyCode.F1] = "F1",
[Enum.KeyCode.F2] = "F2",
[Enum.KeyCode.F3] = "F3",
[Enum.KeyCode.F4] = "F4",
[Enum.KeyCode.F5] = "F5",
[Enum.KeyCode.F6] = "F6",
[Enum.KeyCode.F7] = "F7",
[Enum.KeyCode.F8] = "F8",
[Enum.KeyCode.F9] = "F9",
[Enum.KeyCode.F10] = "F10",
[Enum.KeyCode.F11] = "F11",
[Enum.KeyCode.F12] = "F12",
}
if FFlagProximityPromptMoreKeyCodes2 then
KeyCodeToTextMapping[Enum.KeyCode.PageUp] = "PgUp"
KeyCodeToTextMapping[Enum.KeyCode.PageDown] = "PgDn"
KeyCodeToTextMapping[Enum.KeyCode.Home] = "Home"
KeyCodeToTextMapping[Enum.KeyCode.End] = "End"
KeyCodeToTextMapping[Enum.KeyCode.Insert] = "Ins"
KeyCodeToTextMapping[Enum.KeyCode.Delete] = "Del"
end
local KeyCodeToFontSize = {
[Enum.KeyCode.LeftControl] = 12,
[Enum.KeyCode.RightControl] = 12,
[Enum.KeyCode.LeftAlt] = 12,
[Enum.KeyCode.RightAlt] = 12,
[Enum.KeyCode.F10] = 12,
[Enum.KeyCode.F11] = 12,
[Enum.KeyCode.F12] = 12,
[Enum.KeyCode.PageUp] = 8,
[Enum.KeyCode.PageDown] = 8,
[Enum.KeyCode.Home] = 8,
[Enum.KeyCode.End] = 10,
[Enum.KeyCode.Insert] = 10,
[Enum.KeyCode.Delete] = 10,
}
local function createMainFrame_DEPRECATED()
local frame = Instance.new("Frame")
frame.Name = "ProximityPrompts"
frame.BackgroundTransparency = 1
frame.Size = UDim2.fromScale(1, 1)
frame.Position = UDim2.fromScale(0, 0)
frame.Parent = RobloxGui
return frame
end
local function getScreenGui()
local screenGui = PlayerGui:FindFirstChild("ProximityPrompts")
if screenGui == nil then
screenGui = Instance.new("ScreenGui")
screenGui.Name = "ProximityPrompts"
screenGui.ResetOnSpawn = false
screenGui.Parent = PlayerGui
end
return screenGui
end
local function createProgressBarGradient(parent, leftSide)
local frame = Instance.new("Frame")
frame.Size = UDim2.fromScale(0.5, 1)
frame.Position = UDim2.fromScale(leftSide and 0 or 0.5, 0)
frame.BackgroundTransparency = 1
frame.ClipsDescendants = true
frame.Parent = parent
local image = Instance.new("ImageLabel")
image.BackgroundTransparency = 1
image.Size = UDim2.fromScale(2, 1)
image.Position = UDim2.fromScale(leftSide and 0 or -1, 0)
image.Image = "rbxasset://textures/ui/Controls/RadialFill.png"
image.Parent = frame
local gradient = Instance.new("UIGradient")
gradient.Transparency = NumberSequence.new {
NumberSequenceKeypoint.new(0, 0),
NumberSequenceKeypoint.new(.4999, 0),
NumberSequenceKeypoint.new(.5, 1),
NumberSequenceKeypoint.new(1, 1)
}
gradient.Rotation = leftSide and 180 or 0
gradient.Parent = image
return gradient
end
local function createCircularProgressBar()
local bar = Instance.new("Frame")
bar.Name = "CircularProgressBar"
bar.Size = UDim2.fromOffset(58, 58)
bar.AnchorPoint = Vector2.new(0.5, 0.5)
bar.Position = UDim2.fromScale(0.5, 0.5)
bar.BackgroundTransparency = 1
local gradient1 = createProgressBarGradient(bar, true)
local gradient2 = createProgressBarGradient(bar, false)
local progress = Instance.new("NumberValue")
progress.Name = "Progress"
progress.Parent = bar
progress.Changed:Connect(function(value)
local angle = math.clamp(value * 360, 0, 360)
gradient1.Rotation = math.clamp(angle, 180, 360)
gradient2.Rotation = math.clamp(angle, 0, 180)
end)
return bar
end
-- remove with FFlagProximityPromptLiveChanges
local function createPrompt_DEPRECATED(prompt, inputType)
local tweensForButtonHoldBegin = {}
local tweensForButtonHoldEnd = {}
local tweensForFadeOut = {}
local tweensForFadeIn = {}
local tweenInfoInFullDuration = TweenInfo.new(prompt.HoldDuration, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local tweenInfoOutHalfSecond = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local tweenInfoFast = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local tweenInfoQuick = TweenInfo.new(0.06, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local actionTextSize = TextService:GetTextSize(prompt.ActionText, 19, Enum.Font.GothamSemibold, Vector2.new(1000, 1000))
local objectTextSize = TextService:GetTextSize(prompt.ObjectText, 14, Enum.Font.GothamSemibold, Vector2.new(1000, 1000))
local maxTextWidth = math.max(actionTextSize.X, objectTextSize.X)
local promptHeight = 72
local promptWidth = 72
local textPaddingLeft = 72
if prompt.ActionText ~= nil and prompt.ActionText ~= '' then
promptWidth = maxTextWidth + textPaddingLeft + 24
end
local promptUI = Instance.new("BillboardGui")
promptUI.Name = "Prompt"
promptUI.Adornee = prompt.Parent
promptUI.AlwaysOnTop = true
promptUI.Size = UDim2.fromOffset(promptWidth, promptHeight)
promptUI.SizeOffset = Vector2.new(prompt.UIOffset.X / promptUI.Size.Width.Offset, prompt.UIOffset.Y / promptUI.Size.Height.Offset)
local frame = Instance.new("Frame")
frame.Size = UDim2.fromScale(1, 1)
frame.BackgroundTransparency = 0.2
frame.BackgroundColor3 = Color3.new(0.07, 0.07, 0.07)
frame.Parent = promptUI
local roundedCorner = Instance.new("UICorner")
roundedCorner.Parent = frame
local inputFrame = Instance.new("Frame")
inputFrame.Name = "InputFrame"
inputFrame.Size = UDim2.fromScale(1, 1)
inputFrame.BackgroundTransparency = 1
inputFrame.SizeConstraint = Enum.SizeConstraint.RelativeYY
inputFrame.Parent = frame
local resizeableInputFrame = Instance.new("Frame")
resizeableInputFrame.Size = UDim2.fromScale(1, 1)
resizeableInputFrame.Position = UDim2.fromScale(0.5, 0.5)
resizeableInputFrame.AnchorPoint = Vector2.new(0.5, 0.5)
resizeableInputFrame.BackgroundTransparency = 1
resizeableInputFrame.Parent = inputFrame
local inputFrameScaler = Instance.new("UIScale")
inputFrameScaler.Parent = resizeableInputFrame
local inputFrameScaleFactor = inputType == Enum.ProximityPromptInputType.Touch and 1.6 or 1.33
table.insert(tweensForButtonHoldBegin, TweenService:Create(inputFrameScaler, tweenInfoFast, { Scale = inputFrameScaleFactor }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(inputFrameScaler, tweenInfoFast, { Scale = 1 }))
if prompt.ActionText ~= nil and prompt.ActionText ~= '' then
local actionText = Instance.new("TextLabel")
actionText.Name = "ActionText"
actionText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, 0)
actionText.Size = UDim2.fromScale(1, 1)
actionText.Font = Enum.Font.GothamSemibold
actionText.TextSize = 19
actionText.BackgroundTransparency = 1
actionText.TextColor3 = Color3.new(1, 1, 1)
actionText.TextXAlignment = Enum.TextXAlignment.Left
actionText.Text = prompt.ActionText
if FFlagProximityPromptLocalization then
actionText.AutoLocalize = prompt.AutoLocalize
actionText.RootLocalizationTable = prompt.RootLocalizationTable
end
actionText.Parent = frame
table.insert(tweensForButtonHoldBegin, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 0 }))
table.insert(tweensForFadeOut, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 0 }))
if prompt.ObjectText ~= nil and prompt.ObjectText ~= '' then
actionText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, 9)
local objectText = Instance.new("TextLabel")
objectText.Name = "ObjectText"
objectText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, -10)
objectText.Size = UDim2.fromScale(1, 1)
objectText.Font = Enum.Font.GothamSemibold
objectText.TextSize = 14
objectText.BackgroundTransparency = 1
objectText.TextColor3 = Color3.new(0.7, 0.7, 0.7)
objectText.TextXAlignment = Enum.TextXAlignment.Left
objectText.Text = prompt.ObjectText
if FFlagProximityPromptLocalization then
objectText.AutoLocalize = prompt.AutoLocalize
objectText.RootLocalizationTable = prompt.RootLocalizationTable
end
objectText.Parent = frame
table.insert(tweensForButtonHoldBegin, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 0 }))
table.insert(tweensForFadeOut, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 0 }))
end
table.insert(tweensForButtonHoldBegin, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(0.5, 1), BackgroundTransparency = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 0.2 }))
table.insert(tweensForFadeOut, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(0.5, 1), BackgroundTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 0.2 }))
else
table.insert(tweensForButtonHoldBegin, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 0.2 }))
table.insert(tweensForFadeOut, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(frame, tweenInfoFast, { BackgroundTransparency = 0.2 }))
end
local roundFrame = Instance.new("Frame")
roundFrame.Name = "RoundFrame"
roundFrame.Size = UDim2.fromOffset(48, 48)
roundFrame.AnchorPoint = Vector2.new(0.5, 0.5)
roundFrame.Position = UDim2.fromScale(0.5, 0.5)
roundFrame.BackgroundTransparency = 0.5
roundFrame.Parent = resizeableInputFrame
local roundedFrameCorner = Instance.new("UICorner")
roundedFrameCorner.CornerRadius = UDim.new(0.5, 0)
roundedFrameCorner.Parent = roundFrame
table.insert(tweensForFadeOut, TweenService:Create(roundFrame, tweenInfoQuick, { BackgroundTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(roundFrame, tweenInfoQuick, { BackgroundTransparency = 0.5 }))
if inputType == Enum.ProximityPromptInputType.Gamepad then
if GamepadButtonImage[prompt.GamepadKeyCode] then
local icon = Instance.new("ImageLabel")
icon.Name = "ButtonImage"
icon.AnchorPoint = Vector2.new(0.5, 0.5)
icon.Size = UDim2.fromOffset(24, 24)
icon.Position = UDim2.fromScale(0.5, 0.5)
icon.BackgroundTransparency = 1
icon.Image = GamepadButtonImage[prompt.GamepadKeyCode]
icon.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 0 }))
end
elseif inputType == Enum.ProximityPromptInputType.Touch then
local buttonImage = Instance.new("ImageLabel")
buttonImage.Name = "ButtonImage"
buttonImage.BackgroundTransparency = 1
buttonImage.Size = UDim2.fromOffset(25, 31)
buttonImage.AnchorPoint = Vector2.new(0.5, 0.5)
buttonImage.Position = UDim2.fromScale(0.5, 0.5)
buttonImage.Image = "rbxasset://textures/ui/Controls/TouchTapIcon.png"
buttonImage.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 0 }))
else
local buttonImage = Instance.new("ImageLabel")
buttonImage.Name = "ButtonImage"
buttonImage.BackgroundTransparency = 1
buttonImage.Size = UDim2.fromOffset(28, 30)
buttonImage.AnchorPoint = Vector2.new(0.5, 0.5)
buttonImage.Position = UDim2.fromScale(0.5, 0.5)
buttonImage.Image = "rbxasset://textures/ui/Controls/key_single.png"
buttonImage.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 0 }))
local buttonText = Instance.new("TextLabel")
buttonText.Name = "ButtonText"
buttonText.Position = UDim2.fromOffset(-1, -1)
buttonText.Size = UDim2.fromScale(1, 1)
buttonText.Font = Enum.Font.GothamSemibold
buttonText.TextSize = 14
buttonText.BackgroundTransparency = 1
buttonText.TextColor3 = Color3.new(1, 1, 1)
buttonText.TextXAlignment = Enum.TextXAlignment.Center
buttonText.Text = UserInputService:GetStringForKeyCode(prompt.KeyboardKeyCode)
buttonText.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 0 }))
end
if inputType == Enum.ProximityPromptInputType.Touch or prompt.ClickablePrompt then
local button = Instance.new("TextButton")
button.BackgroundTransparency = 1
button.TextTransparency = 1
button.Size = UDim2.fromScale(1, 1)
button.Parent = promptUI
if FFlagProximityPromptNoButtonDrag then
local buttonDown = false
button.InputBegan:Connect(function(input)
if (input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1) and
input.UserInputState ~= Enum.UserInputState.Change then
prompt:InputHoldBegin()
buttonDown = true
end
end)
button.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
if buttonDown then
buttonDown = false
prompt:InputHoldEnd()
end
end
end)
else
button.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
prompt:InputHoldBegin()
end
end)
button.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
prompt:InputHoldEnd()
end
end)
end
promptUI.Active = true
end
if prompt.HoldDuration > 0 then
local circleBar = createCircularProgressBar()
circleBar.Parent = resizeableInputFrame
table.insert(tweensForButtonHoldBegin, TweenService:Create(circleBar.Progress, tweenInfoInFullDuration, { Value = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(circleBar.Progress, tweenInfoOutHalfSecond, { Value = 0 }))
end
return promptUI, tweensForButtonHoldBegin, tweensForButtonHoldEnd, tweensForFadeOut, tweensForFadeIn
end
local function createPrompt(prompt, inputType, gui)
local tweensForButtonHoldBegin = {}
local tweensForButtonHoldEnd = {}
local tweensForFadeOut = {}
local tweensForFadeIn = {}
local tweenInfoInFullDuration = TweenInfo.new(prompt.HoldDuration, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local tweenInfoOutHalfSecond = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local tweenInfoFast = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local tweenInfoQuick = TweenInfo.new(0.06, Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
local promptUI = Instance.new("BillboardGui")
promptUI.Name = "Prompt"
promptUI.AlwaysOnTop = true
local frame = Instance.new("Frame")
frame.Size = FFlagProximityPromptsFadeIn and UDim2.fromScale(0.5, 1) or UDim2.fromScale(1, 1)
frame.BackgroundTransparency = FFlagProximityPromptsFadeIn and 1 or 0.2
frame.BackgroundColor3 = Color3.new(0.07, 0.07, 0.07)
frame.Parent = promptUI
local roundedCorner = Instance.new("UICorner")
roundedCorner.Parent = frame
local inputFrame = Instance.new("Frame")
inputFrame.Name = "InputFrame"
inputFrame.Size = UDim2.fromScale(1, 1)
inputFrame.BackgroundTransparency = 1
inputFrame.SizeConstraint = Enum.SizeConstraint.RelativeYY
inputFrame.Parent = frame
local resizeableInputFrame = Instance.new("Frame")
resizeableInputFrame.Size = UDim2.fromScale(1, 1)
resizeableInputFrame.Position = UDim2.fromScale(0.5, 0.5)
resizeableInputFrame.AnchorPoint = Vector2.new(0.5, 0.5)
resizeableInputFrame.BackgroundTransparency = 1
resizeableInputFrame.Parent = inputFrame
local inputFrameScaler = Instance.new("UIScale")
inputFrameScaler.Parent = resizeableInputFrame
local inputFrameScaleFactor = inputType == Enum.ProximityPromptInputType.Touch and 1.6 or 1.33
table.insert(tweensForButtonHoldBegin, TweenService:Create(inputFrameScaler, tweenInfoFast, { Scale = inputFrameScaleFactor }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(inputFrameScaler, tweenInfoFast, { Scale = 1 }))
local actionText = Instance.new("TextLabel")
actionText.Name = "ActionText"
actionText.Size = UDim2.fromScale(1, 1)
actionText.Font = Enum.Font.GothamSemibold
actionText.TextSize = 19
actionText.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
actionText.TextTransparency = 1
end
actionText.TextColor3 = Color3.new(1, 1, 1)
actionText.TextXAlignment = Enum.TextXAlignment.Left
actionText.Parent = frame
table.insert(tweensForButtonHoldBegin, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 0 }))
table.insert(tweensForFadeOut, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(actionText, tweenInfoFast, { TextTransparency = 0 }))
local objectText = Instance.new("TextLabel")
objectText.Name = "ObjectText"
objectText.Size = UDim2.fromScale(1, 1)
objectText.Font = Enum.Font.GothamSemibold
objectText.TextSize = 14
objectText.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
objectText.TextTransparency = 1
end
objectText.TextColor3 = Color3.new(0.7, 0.7, 0.7)
objectText.TextXAlignment = Enum.TextXAlignment.Left
objectText.Parent = frame
table.insert(tweensForButtonHoldBegin, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 0 }))
table.insert(tweensForFadeOut, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(objectText, tweenInfoFast, { TextTransparency = 0 }))
table.insert(tweensForButtonHoldBegin, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(0.5, 1), BackgroundTransparency = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 0.2 }))
table.insert(tweensForFadeOut, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(0.5, 1), BackgroundTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(frame, tweenInfoFast, { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 0.2 }))
local roundFrame = Instance.new("Frame")
roundFrame.Name = "RoundFrame"
roundFrame.Size = UDim2.fromOffset(48, 48)
roundFrame.AnchorPoint = Vector2.new(0.5, 0.5)
roundFrame.Position = UDim2.fromScale(0.5, 0.5)
roundFrame.BackgroundTransparency = FFlagProximityPromptsFadeIn and 1 or 0.5
roundFrame.Parent = resizeableInputFrame
local roundedFrameCorner = Instance.new("UICorner")
roundedFrameCorner.CornerRadius = UDim.new(0.5, 0)
roundedFrameCorner.Parent = roundFrame
table.insert(tweensForFadeOut, TweenService:Create(roundFrame, tweenInfoQuick, { BackgroundTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(roundFrame, tweenInfoQuick, { BackgroundTransparency = 0.5 }))
if inputType == Enum.ProximityPromptInputType.Gamepad then
if GamepadButtonImage[prompt.GamepadKeyCode] then
local icon = Instance.new("ImageLabel")
icon.Name = "ButtonImage"
icon.AnchorPoint = Vector2.new(0.5, 0.5)
icon.Size = UDim2.fromOffset(24, 24)
icon.Position = UDim2.fromScale(0.5, 0.5)
icon.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
icon.ImageTransparency = 1
end
icon.Image = GamepadButtonImage[prompt.GamepadKeyCode]
icon.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 0 }))
end
elseif inputType == Enum.ProximityPromptInputType.Touch then
local buttonImage = Instance.new("ImageLabel")
buttonImage.Name = "ButtonImage"
buttonImage.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
buttonImage.ImageTransparency = 1
end
buttonImage.Size = UDim2.fromOffset(25, 31)
buttonImage.AnchorPoint = Vector2.new(0.5, 0.5)
buttonImage.Position = UDim2.fromScale(0.5, 0.5)
buttonImage.Image = "rbxasset://textures/ui/Controls/TouchTapIcon.png"
buttonImage.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 0 }))
else
local buttonImage = Instance.new("ImageLabel")
buttonImage.Name = "ButtonImage"
buttonImage.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
buttonImage.ImageTransparency = 1
end
buttonImage.Size = UDim2.fromOffset(28, 30)
buttonImage.AnchorPoint = Vector2.new(0.5, 0.5)
buttonImage.Position = UDim2.fromScale(0.5, 0.5)
buttonImage.Image = "rbxasset://textures/ui/Controls/key_single.png"
buttonImage.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(buttonImage, tweenInfoQuick, { ImageTransparency = 0 }))
if FFlagProximityPromptMoreKeyCodes then
local buttonTextString = UserInputService:GetStringForKeyCode(prompt.KeyboardKeyCode)
local buttonTextImage = KeyboardButtonImage[prompt.KeyboardKeyCode]
if buttonTextImage == nil then
buttonTextImage = KeyboardButtonIconMapping[buttonTextString]
end
if buttonTextImage == nil then
local keyCodeMappedText = KeyCodeToTextMapping[prompt.KeyboardKeyCode]
if keyCodeMappedText then
buttonTextString = keyCodeMappedText
end
end
if buttonTextImage then
local icon = Instance.new("ImageLabel")
icon.Name = "ButtonImage"
icon.AnchorPoint = Vector2.new(0.5, 0.5)
icon.Size = UDim2.fromOffset(36, 36)
icon.Position = UDim2.fromScale(0.5, 0.5)
icon.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
icon.ImageTransparency = 1
end
icon.Image = buttonTextImage
icon.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(icon, tweenInfoQuick, { ImageTransparency = 0 }))
elseif buttonTextString ~= nil and buttonTextString ~= '' then
local buttonText = Instance.new("TextLabel")
buttonText.Name = "ButtonText"
buttonText.Position = UDim2.fromOffset(0, -1)
buttonText.Size = UDim2.fromScale(1, 1)
buttonText.Font = Enum.Font.GothamSemibold
if FFlagProximityPromptMoreKeyCodes2 then
local buttonTextSize = KeyCodeToFontSize[prompt.KeyboardKeyCode]
if buttonTextSize == nil then
buttonTextSize = 14
end
buttonText.TextSize = buttonTextSize
else
buttonText.TextSize = 14
if string.len(buttonTextString) > 2 then
buttonText.TextSize = 12
end
end
buttonText.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
buttonText.TextTransparency = 1
end
buttonText.TextColor3 = Color3.new(1, 1, 1)
buttonText.TextXAlignment = Enum.TextXAlignment.Center
buttonText.Text = buttonTextString
buttonText.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 0 }))
else
error("ProximityPrompt '" .. prompt.Name .. "' has an unsupported keycode for rendering UI: " .. tostring(prompt.KeyboardKeyCode))
end
else
local buttonText = Instance.new("TextLabel")
buttonText.Name = "ButtonText"
buttonText.Position = UDim2.fromOffset(-1, -1)
buttonText.Size = UDim2.fromScale(1, 1)
buttonText.Font = Enum.Font.GothamSemibold
buttonText.TextSize = 14
buttonText.BackgroundTransparency = 1
if FFlagProximityPromptsFadeIn then
buttonText.TextTransparency = 1
end
buttonText.TextColor3 = Color3.new(1, 1, 1)
buttonText.TextXAlignment = Enum.TextXAlignment.Center
buttonText.Text = UserInputService:GetStringForKeyCode(prompt.KeyboardKeyCode)
buttonText.Parent = resizeableInputFrame
table.insert(tweensForFadeOut, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 1 }))
table.insert(tweensForFadeIn, TweenService:Create(buttonText, tweenInfoQuick, { TextTransparency = 0 }))
end
end
if inputType == Enum.ProximityPromptInputType.Touch or prompt.ClickablePrompt then
local button = Instance.new("TextButton")
button.BackgroundTransparency = 1
button.TextTransparency = 1
button.Size = UDim2.fromScale(1, 1)
button.Parent = promptUI
if FFlagProximityPromptNoButtonDrag then
local buttonDown = false
button.InputBegan:Connect(function(input)
if (input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1) and
input.UserInputState ~= Enum.UserInputState.Change then
prompt:InputHoldBegin()
buttonDown = true
end
end)
button.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
if buttonDown then
buttonDown = false
prompt:InputHoldEnd()
end
end
end)
else
button.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
prompt:InputHoldBegin()
end
end)
button.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then
prompt:InputHoldEnd()
end
end)
end
promptUI.Active = true
end
if prompt.HoldDuration > 0 then
local circleBar = createCircularProgressBar()
circleBar.Parent = resizeableInputFrame
table.insert(tweensForButtonHoldBegin, TweenService:Create(circleBar.Progress, tweenInfoInFullDuration, { Value = 1 }))
table.insert(tweensForButtonHoldEnd, TweenService:Create(circleBar.Progress, tweenInfoOutHalfSecond, { Value = 0 }))
end
local holdBeganConnection
local holdEndedConnection
local triggeredConnection
local triggerEndedConnection
if prompt.HoldDuration > 0 then
holdBeganConnection = prompt.PromptButtonHoldBegan:Connect(function()
for _, tween in ipairs(tweensForButtonHoldBegin) do
tween:Play()
end
end)
holdEndedConnection = prompt.PromptButtonHoldEnded:Connect(function()
for _, tween in ipairs(tweensForButtonHoldEnd) do
tween:Play()
end
end)
end
triggeredConnection = prompt.Triggered:Connect(function()
for _, tween in ipairs(tweensForFadeOut) do
tween:Play()
end
end)
triggerEndedConnection = prompt.TriggerEnded:Connect(function()
for _, tween in ipairs(tweensForFadeIn) do
tween:Play()
end
end)
local function updateUIFromPrompt()
-- todo: Use AutomaticSize instead of GetTextSize when that feature becomes available
local actionTextSize = TextService:GetTextSize(prompt.ActionText, 19, Enum.Font.GothamSemibold, Vector2.new(1000, 1000))
local objectTextSize = TextService:GetTextSize(prompt.ObjectText, 14, Enum.Font.GothamSemibold, Vector2.new(1000, 1000))
local maxTextWidth = math.max(actionTextSize.X, objectTextSize.X)
local promptHeight = 72
local promptWidth = 72
local textPaddingLeft = 72
if (prompt.ActionText ~= nil and prompt.ActionText ~= '') or
(prompt.ObjectText ~= nil and prompt.ObjectText ~= '') then
promptWidth = maxTextWidth + textPaddingLeft + 24
end
local actionTextYOffset = 0
if prompt.ObjectText ~= nil and prompt.ObjectText ~= '' then
actionTextYOffset = 9
end
actionText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, actionTextYOffset)
objectText.Position = UDim2.new(0.5, textPaddingLeft - promptWidth/2, 0, -10)
actionText.Text = prompt.ActionText
objectText.Text = prompt.ObjectText
if FFlagProximityPromptLocalization then
actionText.AutoLocalize = prompt.AutoLocalize
actionText.RootLocalizationTable = prompt.RootLocalizationTable
objectText.AutoLocalize = prompt.AutoLocalize
objectText.RootLocalizationTable = prompt.RootLocalizationTable
end
promptUI.Size = UDim2.fromOffset(promptWidth, promptHeight)
promptUI.SizeOffset = Vector2.new(prompt.UIOffset.X / promptUI.Size.Width.Offset, prompt.UIOffset.Y / promptUI.Size.Height.Offset)
end
local changedConnection = prompt.Changed:Connect(updateUIFromPrompt)
updateUIFromPrompt()
promptUI.Adornee = prompt.Parent
promptUI.Parent = gui
if FFlagProximityPromptsFadeIn then
for _, tween in ipairs(tweensForFadeIn) do
tween:Play()
end
end
local function cleanup()
if holdBeganConnection then
holdBeganConnection:Disconnect()
end
if holdEndedConnection then
holdEndedConnection:Disconnect()
end
triggeredConnection:Disconnect()
triggerEndedConnection:Disconnect()
changedConnection:Disconnect()
if FFlagProximityPromptsFadeIn then
for _, tween in ipairs(tweensForFadeOut) do
tween:Play()
end
wait(0.2)
end
promptUI.Parent = nil
end
return cleanup
end
local function onLoad()
local gui
if not FFlagProximityPromptLocalization then
gui = createMainFrame_DEPRECATED()
end
ProximityPromptService.PromptShown:Connect(function(prompt, inputType)
if prompt.Style == Enum.ProximityPromptStyle.Custom then
return
end
if FFlagProximityPromptLocalization then
gui = getScreenGui()
end
if FFlagProximityPromptLiveChanges then
local cleanupFunction = createPrompt(prompt, inputType, gui)
prompt.PromptHidden:Wait()
cleanupFunction()
else
local promptUI, tweensForButtonHoldBegin, tweensForButtonHoldEnd, tweensForFadeOut, tweensForFadeIn = createPrompt_DEPRECATED(prompt, inputType)
promptUI.Parent = gui
local holdBeganConnection
local holdEndedConnection
local triggeredConnection
local triggerEndedConnection
if prompt.HoldDuration > 0 then
holdBeganConnection = prompt.PromptButtonHoldBegan:Connect(function()
for _, tween in ipairs(tweensForButtonHoldBegin) do
tween:Play()
end
end)
holdEndedConnection = prompt.PromptButtonHoldEnded:Connect(function()
for _, tween in ipairs(tweensForButtonHoldEnd) do
tween:Play()
end
end)
end
triggeredConnection = prompt.Triggered:Connect(function()
for _, tween in ipairs(tweensForFadeOut) do
tween:Play()
end
end)
triggerEndedConnection = prompt.TriggerEnded:Connect(function()
for _, tween in ipairs(tweensForFadeIn) do
tween:Play()
end
end)
prompt.PromptHidden:Wait()
if holdBeganConnection then
holdBeganConnection:Disconnect()
end
if holdEndedConnection then
holdEndedConnection:Disconnect()
end
triggeredConnection:Disconnect()
triggerEndedConnection:Disconnect()
promptUI.Parent = nil
end
end)
end
onLoad()
@@ -0,0 +1,205 @@
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local NotificationService = game:GetService("NotificationService")
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local ScreenTimeHttpRequests = require(CorePackages.Regulations.ScreenTime.HttpRequests)
local ScreenTimeConstants = require(CorePackages.Regulations.ScreenTime.Constants)
local GetFFlagScreenTimeSignalR = require(CorePackages.Regulations.ScreenTime.GetFFlagScreenTimeSignalR)
local Logging = require(CorePackages.Logging)
local ErrorPrompt = require(RobloxGui.Modules.ErrorPrompt)
local Url = require(RobloxGui.Modules.Common.Url)
local function leaveGame()
GuiService.SelectedCoreObject = nil
game:Shutdown()
end
local ScreenTimeState = {
Warning = 1,
Lockout = 2,
OpenWebView = 3,
}
local TAG = "ScreenTimeInGame"
local screenTimeHttpRequests = ScreenTimeHttpRequests:new(HttpService)
--[[
Resolving message:
* display (move message PendingResolve -> Displaying)
* report execution - report as soon as displayed to resolve with server
* user input - pressing "ok" (move message Displaying -> Resolved)
]]
local messageQueue = {
pendingResolveMessage = {},
displaying = nil,
resolvedMessage = {},
displayMessageCallback = nil,
update = function(self, instructions)
for _, instruction in pairs(instructions) do
local pendingMessage = {
id = instruction.serialId,
instructionName = instruction.instructionName,
message = instruction.message,
}
table.insert(self.pendingResolveMessage, pendingMessage)
end
self:processNext()
end,
processNext = function(self)
if self.displaying then -- dialog is showing
return
end
local messageToDisplay = table.remove(self.pendingResolveMessage, 1)
if not messageToDisplay then -- table is empty
return
end
if self.resolvedMessage[messageToDisplay.id] then -- message was displayed before
self:processNext()
return
end
self.displaying = messageToDisplay
if self.displayMessageCallback then
self.displayMessageCallback(messageToDisplay.message)
end
end,
-- currently messages will be resolved on client side by user click on the "OK" button
resolve = function(self)
self.resolvedMessage[self.displaying.id] = self.displaying.message
-- Report execution after user interacted
screenTimeHttpRequests:reportExecution(self.displaying.instructionName, self.displaying.id)
self.displaying = nil
self:processNext()
end,
}
-- Setting up the prompt and connect callbacks
local extraConfiguration = {
MessageTextScaled = true,
HideErrorCode = true,
MenuIsOpenKey = "ScreenTimePrompt",
}
local prompt = ErrorPrompt.new("Default", extraConfiguration)
prompt:setParent(RobloxGui)
local function displayMessage(message)
prompt:_open(message)
end
local function resolveMessage()
prompt:_close()
messageQueue:resolve()
end
local buttonList = {
{
Text = "OK",
LocalizationKey = "InGame.CommonUI.Button.Ok",
LayoutOrder = 1,
Callback = resolveMessage,
Primary = true,
}
}
messageQueue.displayMessageCallback = displayMessage
prompt:updateButtons(buttonList)
prompt:setErrorTitle("Warning", "InGame.CommonUI.Title.Warning")
local screenWidth = RobloxGui.AbsoluteSize.X
local function onScreenSizeChanged()
if not prompt then
return
end
local newWidth = RobloxGui.AbsoluteSize.X
if screenWidth ~= newWidth then
screenWidth = newWidth
prompt:resizeWidth(screenWidth)
end
end
RobloxGui:GetPropertyChangedSignal("AbsoluteSize"):connect(onScreenSizeChanged)
onScreenSizeChanged()
local screenTimeUpdatedConnection
local function screenTimeStatesUpdated(instructions)
local lockout = false
local filteredInstructions = {}
for _, instruction in ipairs(instructions) do
if instruction.type == ScreenTimeState.Warning then
table.insert(filteredInstructions, instruction)
elseif instruction.type == ScreenTimeState.Lockout then
-- If there is a lockout, we will stop getting other state and then leaveGame
lockout = true
break
elseif instruction.type == ScreenTimeState.OpenWebView then
-- Dont process it in games according to the requirement.
-- Doc: https://luobu.atlassian.net/l/c/BJ0vnXZi
-- It will be processed by other codes when user is not in any game.
end
end
if lockout then
-- immediately quit when locked out
if screenTimeUpdatedConnection then
screenTimeUpdatedConnection:Disconnect()
end
leaveGame()
else
messageQueue:update(filteredInstructions)
end
end
local function requestInstructions()
screenTimeHttpRequests:getInstructions(function(success, unauthorized, instructions)
if success then
screenTimeStatesUpdated(instructions)
elseif unauthorized then
-- Leave it to LuaApp
Logging.warn(TAG .. " requestInstructions failed: unauthorized")
else
Logging.warn(TAG .. " requestInstructions failed: error")
end
end)
end
screenTimeUpdatedConnection = NotificationService.RobloxEventReceived:Connect(function(eventData)
if GetFFlagScreenTimeSignalR() then
if eventData.namespace == ScreenTimeConstants.SIGNALR_NAMESPACE and
eventData.detailType == ScreenTimeConstants.SIGNALR_TYPE_NEW_INSTRUCTION then
requestInstructions()
end
else
if eventData.namespace == ScreenTimeConstants.HEARTBEAT_NOTIFICATIONS_NAMESPACE then
local success, json = pcall(function()
return HttpService:JSONDecode(eventData.detail)
end)
if not success then
Logging.warn(TAG .. " json decoding failed")
return
end
-- "errorCode" has been checked by C++, so no need to recheck.
if json.notifications == nil then
Logging.warn(TAG .. " empty heartbeat notifications")
return
end
for _, notification in ipairs(json.notifications) do
if notification.type == ScreenTimeConstants.HEARTBEAT_NOTIFICATION_TYPE_NEW_INSTRUCTION then
requestInstructions()
end
end
end
end
end)
@@ -0,0 +1,155 @@
--[[
// Filename: VehicleHud.lua
// Version 1.0
// Written by: jmargh
// Description: Implementation of the VehicleSeat HUD
// TODO:
Once this is live and stable, move to PlayerScripts as module
]]
local RunService = game:GetService('RunService')
local Players = game:GetService('Players')
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
local RobloxGui = script.Parent
local CurrentVehicleSeat = nil
local VehicleSeatHeartbeatCn = nil
local VehicleSeatHUDChangedCn = nil
local RobloxGui = game:GetService("CoreGui"):WaitForChild("RobloxGui")
RobloxGui:WaitForChild("Modules"):WaitForChild("TenFootInterface")
local isTenFootInterface = require(RobloxGui.Modules.TenFootInterface):IsEnabled()
--[[ Images ]]--
local VEHICLE_HUD_BG = 'rbxasset://textures/ui/Vehicle/SpeedBarBKG.png'
local SPEED_BAR_EMPTY = 'rbxasset://textures/ui/Vehicle/SpeedBarEmpty.png'
local SPEED_BAR = 'rbxasset://textures/ui/Vehicle/SpeedBar.png'
--[[ Constants ]]--
local BOTTOM_OFFSET = (isTenFootInterface and 100 or 70)
--[[ Gui Creation ]]--
local function createImageLabel(name, size, position, image, parent)
local imageLabel = Instance.new('ImageLabel')
imageLabel.Name = name
imageLabel.Size = size
imageLabel.Position = position
imageLabel.BackgroundTransparency = 1
imageLabel.Image = image
imageLabel.Parent = parent
return imageLabel
end
local function createTextLabel(name, alignment, text, parent)
local textLabel = Instance.new('TextLabel')
textLabel.Name = name
textLabel.Size = UDim2.new(1, -4, 0, (isTenFootInterface and 50 or 20))
textLabel.Position = UDim2.new(0, 2, 0, (isTenFootInterface and -50 or -20))
textLabel.BackgroundTransparency = 1
textLabel.TextXAlignment = alignment
textLabel.Font = Enum.Font.SourceSans
textLabel.FontSize = (isTenFootInterface and Enum.FontSize.Size48 or Enum.FontSize.Size18)
textLabel.TextColor3 = Color3.new(1, 1, 1)
textLabel.TextStrokeTransparency = 0.5
textLabel.TextStrokeColor3 = Color3.new(49/255, 49/255, 49/255)
textLabel.Text = text
textLabel.Parent = parent
return textLabel
end
local VehicleHudFrame = Instance.new('Frame')
VehicleHudFrame.Name = "VehicleHudFrame"
VehicleHudFrame.Size = UDim2.new(0, (isTenFootInterface and 316 or 158), 0, (isTenFootInterface and 50 or 14))
VehicleHudFrame.Position = UDim2.new(0.5, -(VehicleHudFrame.Size.X.Offset/2), 1, -BOTTOM_OFFSET - VehicleHudFrame.Size.Y.Offset)
VehicleHudFrame.BackgroundTransparency = 1
VehicleHudFrame.Visible = false
VehicleHudFrame.Parent = RobloxGui
local speedBarClippingFrame = Instance.new("Frame")
speedBarClippingFrame.Name = "SpeedBarClippingFrame"
speedBarClippingFrame.Size = UDim2.new(0, 0, 0, (isTenFootInterface and 24 or 4))
speedBarClippingFrame.Position = UDim2.new(0.5, (isTenFootInterface and -142 or -71), 0.5, (isTenFootInterface and -13 or -2))
speedBarClippingFrame.BackgroundTransparency = 1
speedBarClippingFrame.ClipsDescendants = true
speedBarClippingFrame.ZIndex = 2
speedBarClippingFrame.Parent = VehicleHudFrame
local HudBG = createImageLabel("HudBG", UDim2.new(1, 0, 1, 0), UDim2.new(0, 0, 0, 1), VEHICLE_HUD_BG, VehicleHudFrame)
local SpeedBG = createImageLabel("SpeedBG", UDim2.new(0, (isTenFootInterface and 284 or 142), 0, (isTenFootInterface and 24 or 4)), UDim2.new(0.5, (isTenFootInterface and -142 or -71), 0.5, (isTenFootInterface and -13 or -2)), SPEED_BAR_EMPTY, VehicleHudFrame)
local SpeedBarImage = createImageLabel("SpeedBarImage", UDim2.new(0, (isTenFootInterface and 284 or 142), 1, 0), UDim2.new(0, 0, 0, 0), SPEED_BAR, speedBarClippingFrame)
local SpeedLabel = createTextLabel("SpeedLabel", Enum.TextXAlignment.Left, "Speed", VehicleHudFrame)
local SpeedText = createTextLabel("SpeedText", Enum.TextXAlignment.Right, "0", VehicleHudFrame)
--[[ Local Functions ]]--
local function getHumanoid()
local character = LocalPlayer and LocalPlayer.Character
if character then
for _,child in pairs(character:GetChildren()) do
if child:IsA('Humanoid') then
return child
end
end
end
end
local function onHeartbeat()
if CurrentVehicleSeat then
local speed = CurrentVehicleSeat.Velocity.magnitude
SpeedText.Text = tostring(math.min(math.floor(speed), 9999))
local drawSize = math.floor((speed / CurrentVehicleSeat.MaxSpeed) * SpeedBG.Size.X.Offset)
drawSize = math.min(drawSize, SpeedBG.Size.X.Offset)
speedBarClippingFrame.Size = UDim2.new(0, drawSize, 0, (isTenFootInterface and 24 or 4))
end
end
local function onVehicleSeatChanged(property)
if property == "HeadsUpDisplay" then
VehicleHudFrame.Visible = not VehicleHudFrame.Visible
end
end
local function onSeated(active, currentSeatPart)
if active then
if currentSeatPart and currentSeatPart:IsA('VehicleSeat') then
CurrentVehicleSeat = currentSeatPart
VehicleHudFrame.Visible = CurrentVehicleSeat.HeadsUpDisplay
VehicleSeatHeartbeatCn = RunService.Heartbeat:connect(onHeartbeat)
VehicleSeatHUDChangedCn = CurrentVehicleSeat.Changed:connect(onVehicleSeatChanged)
end
else
if CurrentVehicleSeat then
VehicleHudFrame.Visible = false
CurrentVehicleSeat = nil
if VehicleSeatHeartbeatCn then
VehicleSeatHeartbeatCn:disconnect()
VehicleSeatHeartbeatCn = nil
end
if VehicleSeatHUDChangedCn then
VehicleSeatHUDChangedCn:disconnect()
VehicleSeatHUDChangedCn = nil
end
end
end
end
local function connectSeated()
local humanoid = getHumanoid()
while not humanoid do
wait()
humanoid = getHumanoid()
end
humanoid.Seated:connect(onSeated)
end
if LocalPlayer.Character then
connectSeated()
end
LocalPlayer.CharacterAdded:connect(function(character)
onSeated(false)
connectSeated()
end)
@@ -0,0 +1,998 @@
-- Creates the generic "ROBLOX" loading screen on startup
-- Written by ArceusInator & Ben Tkacheff, 2014
-- Updates by 0xBAADF00D, 2017
local AssetService = game:GetService("AssetService")
local MarketplaceService = game:GetService("MarketplaceService")
local UserInputService = game:GetService("UserInputService")
local VRService = game:GetService("VRService")
local GuiService = game:GetService("GuiService")
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local ContentProvider = game:GetService("ContentProvider")
local CoreGui = game:GetService("CoreGui")
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ScriptContext = game:GetService("ScriptContext")
local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local create = require(RobloxGui:WaitForChild("Modules"):WaitForChild("Common"):WaitForChild("Create"))
local PolicyService = require(RobloxGui.Modules.Common:WaitForChild("PolicyService"))
--FFlags
local FFlagLoadTheLoadingScreenFasterSuccess, FFlagLoadTheLoadingScreenFasterValue = pcall(function() return settings():GetFFlag("LoadTheLoadingScreenFaster") end)
local FFlagLoadTheLoadingScreenFaster = FFlagLoadTheLoadingScreenFasterSuccess and FFlagLoadTheLoadingScreenFasterValue
local FFlagLoadTheLoadingScreenEvenFaster = game:DefineFastFlag("LoadTheLoadingScreenEvenFaster", false)
local FFlagLoadingScreenDontBlockOnPolicyService = game:DefineFastFlag("LoadingScreenDontBlockOnPolicyService", false)
local FFlagBackButtonWhileLoadingGoesBackToApp = game:DefineFastFlag("BackButtonWhileLoadingGoesBackToApp", false)
local FFlagLoadingScreenShowBlankUntilPolicyServiceReturns = game:DefineFastFlag("LoadingScreenShowBlankUntilPolicyServiceReturns", false)
local FFlagShowConnectionErrorCode = settings():GetFFlag("ShowConnectionErrorCode")
local FFlagConnectionScriptEnabled = settings():GetFFlag("ConnectionScriptEnabled")
local antiAddictionNoticeStringEn = "Boycott bad games, refuse pirated games. Be aware of self-defense and being deceived. Playing games is good for your brain, but too much game play can harm your health. Manage your time well and enjoy a healthy lifestyle."
local FFlagConnectErrorHandlerInLoadingScript = require(RobloxGui.Modules.Flags.FFlagConnectErrorHandlerInLoadingScript)
local loadErrorHandlerFromEngine = game:GetEngineFeature("LoadErrorHandlerFromEngine")
local debugMode = false
local startTime = tick()
local loadingImageInputBeganConn = nil
local waitForGameIdConnection
local GAME_THUMBNAIL_URL = "rbxthumb://type=GameIcon&id=%d&w=256&h=256"
local MAX_ICON_SIZE = Vector2.new(256, 256)
local ICON_ASPECT_RATIO = 1
local COLORS = {
BACKGROUND_COLOR = Color3.fromRGB(45, 45, 45),
TEXT_COLOR = Color3.fromRGB(255, 255, 255),
ERROR = Color3.fromRGB(253, 68, 72)
}
local spinnerImageId = "rbxasset://textures/loading/robloxTilt.png"
-- Variables
local GameAssetInfo -- loaded by InfoProvider:LoadAssets()
local currScreenGui
local renderSteppedConnection
local backButtonConnection
local destroyingBackground, destroyedLoadingGui = false, false
local isTenFootInterface = GuiService:IsTenFootInterface()
local placeLabel, creatorLabel = nil, nil
local backgroundFadeStarted = false
local tweenPlaceIcon = nil
local layoutIsReady = false
local connectionHealthShown = false
local connectionHealthCon
local InfoProvider = {}
local function WaitForPlaceId()
local placeId = game.PlaceId
if placeId == 0 then
game:GetPropertyChangedSignal("PlaceId"):wait()
placeId = game.PlaceId
end
return placeId
end
function InfoProvider:GetGameName()
if GameAssetInfo ~= nil then
return GameAssetInfo.Name
else
return ''
end
end
function InfoProvider:GetCreatorName()
if GameAssetInfo ~= nil then
return GameAssetInfo.Creator.Name
else
return ''
end
end
function InfoProvider:LoadAssets()
if FFlagLoadTheLoadingScreenFaster then
coroutine.wrap(function()
local placeId = WaitForPlaceId()
local success, result = pcall(function()
GameAssetInfo = MarketplaceService:GetProductInfo(placeId)
end)
if not success then
print("LoadingScript->InfoProvider:LoadAssets:", result)
end
end)()
else
--spawn() == slowpoke.jpg
spawn(function()
local PLACEID = game.PlaceId
if PLACEID <= 0 then
while game.PlaceId <= 0 do
wait()
end
PLACEID = game.PlaceId
end
-- load game asset info
coroutine.resume(coroutine.create(function()
local success, result = pcall(function()
GameAssetInfo = MarketplaceService:GetProductInfo(PLACEID)
end)
if not success then
print("LoadingScript->InfoProvider:LoadAssets:", result)
end
end))
end)
end
end
-- create a cancel binding for console to be able to cancel anytime while loading
local function createTenfootCancelGui()
local cancelLabel = create'ImageLabel'
{
Name = "CancelLabel",
Size = UDim2.new(0, 83, 0, 83),
Position = UDim2.new(1, -32 - 83, 0, 32),
BackgroundTransparency = 1,
Image = 'rbxasset://textures/ui/Shell/ButtonIcons/BButton.png'
}
local cancelText = create'TextLabel'
{
Name = "CancelText",
Size = UDim2.new(0, 400, 0, 83),
Position = UDim2.new(1, -131, 0, 32),
AnchorPoint = Vector2.new(1, 0),
BackgroundTransparency = 1,
Font = Enum.Font.SourceSans,
TextSize = 48,
TextXAlignment = Enum.TextXAlignment.Right,
TextColor3 = COLORS.TEXT_COLOR,
Text = "Cancel"
}
if not ReplicatedFirst:IsFinishedReplicating() then
local seenBButtonBegin = false
ContextActionService:BindCoreAction("CancelGameLoad",
function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenBButtonBegin = true
elseif inputState == Enum.UserInputState.End and seenBButtonBegin then
cancelLabel:Destroy()
cancelText.Text = "Canceling..."
cancelText.Position = UDim2.new(1, -32, 0, 32)
ContextActionService:UnbindCoreAction('CancelGameLoad')
game:Shutdown()
end
end,
false,
Enum.KeyCode.ButtonB)
end
while cancelLabel.Parent == nil do
if currScreenGui then
local blackFrame = currScreenGui:FindFirstChild('BlackFrame')
if blackFrame then
cancelLabel.Parent = blackFrame
cancelText.Parent = blackFrame
break
end
end
wait()
end
end
local function GenerateGui()
local screenGui = create 'ScreenGui' {
Name = 'RobloxLoadingGui',
DisplayOrder = 8,
}
--
-- create descendant frames
local mainBackgroundContainer
local inGameGlobalGuiInset = settings():GetFVariable("InGameGlobalGuiInset")
mainBackgroundContainer = create 'Frame' {
Name = 'BlackFrame',
BackgroundColor3 = COLORS.BACKGROUND_COLOR,
BackgroundTransparency = 0,
Size = UDim2.new(1, 0, 1, inGameGlobalGuiInset),
Position = UDim2.new(0, 0, 0, -inGameGlobalGuiInset),
Active = true,
Parent = screenGui
}
local closeButton = create 'ImageButton' {
Name = 'CloseButton',
Image = 'rbxasset://textures/loading/cancelButton.png',
ImageTransparency = 1,
BackgroundTransparency = 1,
Position = UDim2.new(1, -37, 0, 5),
Size = UDim2.new(0, 32, 0, 32),
Active = false,
ZIndex = 10,
Parent = mainBackgroundContainer
}
closeButton.MouseButton1Click:connect(function()
game:Shutdown()
end)
local graphicsFrame = create 'Frame' {
Name = 'GraphicsFrame',
BorderSizePixel = 0,
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(1, 1),
Position = UDim2.new(0.95, 0, 0.95, 0),
Size = UDim2.new(0.15, 0, 0.15, 0),
ZIndex = 2,
Parent = mainBackgroundContainer,
create("UIAspectRatioConstraint") {
AspectRatio = 1
},
create("UISizeConstraint") {
MaxSize = Vector2.new(100, 100)
}
}
local loadingImage = create 'ImageLabel' {
Name = 'LoadingImage',
BackgroundTransparency = 1,
Image = spinnerImageId,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 2,
Parent = graphicsFrame,
}
local numberOfTaps = 0
local lastTapTime = math.huge
local doubleTapTimeThreshold = 0.5
loadingImageInputBeganConn = loadingImage.InputBegan:connect(function()
if PolicyService:IsSubjectToChinaPolicies() then
return
end
if numberOfTaps == 0 then
numberOfTaps = 1
lastTapTime = tick()
return
end
if UserInputService.TouchEnabled == true and UserInputService.MouseEnabled == false then
if tick() - lastTapTime <= doubleTapTimeThreshold then
GuiService:ShowStatsBasedOnInputString("ConnectionHealth")
connectionHealthShown = not connectionHealthShown
end
end
numberOfTaps = 0
lastTapTime = math.huge
end)
local infoFrame = create 'Frame' {
Name = 'InfoFrame',
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(0.75, 0, 1, 0),
ZIndex = 2,
Parent = mainBackgroundContainer,
create 'UIPadding' {
Name = 'UiMessagePadding',
PaddingBottom = UDim.new(0, 25),
} or nil
}
local uiMessageFrame = create 'Frame' {
Name = 'UiMessageFrame',
BackgroundTransparency = 1,
Position = UDim2.new(0.25, 0, 1, -120),
Size = UDim2.new(1, 0, 0, 35),
ZIndex = 2,
LayoutOrder = 5,
Parent = infoFrame,
create 'TextLabel' {
Name = 'UiMessage',
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, 5),
Size = UDim2.new(1, 0, 0, 25),
Font = Enum.Font.SourceSansLight,
TextSize = 18,
TextScaled = true,
TextWrapped = true,
TextColor3 = COLORS.TEXT_COLOR,
Text = "",
TextTransparency = 1,
ZIndex = 2,
}
}
local infoFrameAspect = create("UIAspectRatioConstraint") {
AspectRatio = 3 / 2,
Parent = infoFrame
}
local infoFrameList = create("UIListLayout") {
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Vertical,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
Padding = UDim.new(0.05, 0),
Parent = infoFrame
}
local textContainer = create("Frame") {
BackgroundTransparency = 1,
Size = UDim2.new(2/3, 0, 1, 0),
LayoutOrder = 2,
Parent = nil,
create("UIListLayout") {
FillDirection = Enum.FillDirection.Vertical,
VerticalAlignment = Enum.VerticalAlignment.Center,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder
}
}
local placeIcon = create("ImageLabel") {
Name = "PlaceIcon",
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
LayoutOrder = 1,
Parent = infoFrame,
ImageTransparency = 1,
Image = "",
create("UIAspectRatioConstraint") {
AspectRatio = FFlagLoadTheLoadingScreenEvenFaster and ICON_ASPECT_RATIO or 576 / 324,
AspectType = Enum.AspectType.ScaleWithParentSize,
DominantAxis = Enum.DominantAxis.Width
},
create("UISizeConstraint") {
MaxSize = FFlagLoadTheLoadingScreenEvenFaster and MAX_ICON_SIZE or Vector2.new(400, 400)
}
}
if FFlagLoadTheLoadingScreenEvenFaster then
local function onGameId()
local gameId = game.GameId
local imageUrl = GAME_THUMBNAIL_URL:format(gameId)
placeIcon.Image = imageUrl
ContentProvider:PreloadAsync({ placeIcon })
if not backgroundFadeStarted then
placeIcon.ImageTransparency = 0
end
end
if game.GameId > 0 then
coroutine.wrap(onGameId)()
else
waitForGameIdConnection = game:GetPropertyChangedSignal("GameId"):Connect(function ()
waitForGameIdConnection:Disconnect()
onGameId()
end)
end
else
--Start trying to load the place icon image
--Web might not have this icon size generated, so we can poll asset-thumbnail/json and check
--the JSON result for thumbnailFinal/Final to see when it's done being generated so we never
--show a N/A image. This is how the console AppShell does it!
coroutine.wrap(function()
local placeId = WaitForPlaceId()
local function tryGetFinalAsync()
local imageUrl = nil
local isGenerated = false
local success, msg = pcall(function()
imageUrl, isGenerated = AssetService:GetAssetThumbnailAsync(placeId, Vector2.new(576, 324), 1)
end)
if success and isGenerated == true and imageUrl then
ContentProvider:PreloadAsync { imageUrl }
placeIcon.Image = imageUrl
if not backgroundFadeStarted then
placeIcon.ImageTransparency = 0
end
return true
end
return false
end
while not tryGetFinalAsync() do end
end)()
end
placeLabel = create 'TextLabel' {
Name = 'PlaceLabel',
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 80),
Position = UDim2.new(0, 0, 0, 0),
Font = Enum.Font.SourceSans,
TextSize = isTenFootInterface and 48 or 24,
TextWrapped = true,
TextScaled = true,
TextColor3 = COLORS.TEXT_COLOR,
TextStrokeTransparency = 1,
TextTransparency = 1,
Text = "",
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Bottom,
ZIndex = 2,
LayoutOrder = 2,
Parent = infoFrame
}
local creatorContainer = create 'Frame' {
Name = "Creator",
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 48),
LayoutOrder = 3,
create 'UIListLayout' {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5)
}
}
local byLabel, creatorIcon = nil, nil
if isTenFootInterface then
byLabel = create'TextLabel' {
Name = "ByLabel",
BackgroundTransparency = 1,
Size = UDim2.new(0, 36, 0, 30),
Position = UDim2.new(0, 0, 0, 80),
Font = Enum.Font.SourceSansLight,
TextSize = 36,
TextScaled = true,
TextColor3 = COLORS.TEXT_COLOR,
TextStrokeTransparency = 1,
Text = "By",
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
ZIndex = 2,
Visible = true,
Parent = infoFrame,
LayoutOrder = 1
}
creatorIcon = create'ImageLabel' {
Name = "CreatorIcon",
BackgroundTransparency = 1,
Size = UDim2.new(0, 30, 0, 30),
Position = UDim2.new(0, 38, 0, 80),
ImageTransparency = 0,
Image = 'rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png',
ZIndex = 2,
Visible = true,
Parent = infoFrame,
LayoutOrder = 2
}
end
creatorLabel = create 'TextLabel' {
Name = 'CreatorLabel',
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 30),
Position = UDim2.new(0, 0, 0, 80),
Font = Enum.Font.SourceSansLight,
TextSize = isTenFootInterface and 36 or 18,
TextWrapped = true,
TextScaled = true,
TextColor3 = COLORS.TEXT_COLOR,
TextStrokeTransparency = 1,
Text = "",
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
ZIndex = 2,
LayoutOrder = 4,
Parent = infoFrame
}
if isTenFootInterface then
creatorContainer.Parent = infoFrame
byLabel.TextScaled = false
byLabel.Parent = creatorContainer
byLabel.TextXAlignment = Enum.TextXAlignment.Center
byLabel.TextYAlignment = Enum.TextYAlignment.Center
creatorIcon.Parent = creatorContainer
creatorLabel.Parent = creatorContainer
creatorLabel.TextScaled = false
creatorLabel.TextWrapped = false
creatorLabel.Position = UDim2.new(0, 72, 0, 80)
creatorLabel.Size = UDim2.new(0, creatorLabel.TextBounds.X, 1, 0)
end
coroutine.wrap(function()
RunService.RenderStepped:wait()
RunService.RenderStepped:wait()
layoutIsReady = true
placeLabel.TextTransparency = 0
local uiMessage = uiMessageFrame.UiMessage
if uiMessage.Text ~= "" then
uiMessage.TextTransparency = 0
end
end)()
if not FFlagConnectionScriptEnabled or isTenFootInterface then
local errorFrame = create 'Frame' {
Name = 'ErrorFrame',
BackgroundColor3 = COLORS.ERROR,
BorderSizePixel = 0,
Position = UDim2.new(0.25,0,0,0),
Size = UDim2.new(0.5, 0, 0, 80),
ZIndex = 8,
Visible = false,
Parent = screenGui,
create 'TextLabel' {
Name = "ErrorText",
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Font = Enum.Font.SourceSansBold,
TextSize = 14,
TextWrapped = true,
TextColor3 = COLORS.TEXT_COLOR,
Text = "",
ZIndex = 8
}
}
else
if not loadErrorHandlerFromEngine and FFlagConnectErrorHandlerInLoadingScript then
ScriptContext:AddCoreScriptLocal("Connection", RobloxGui)
end
end
while not game:GetService("CoreGui") do
if FFlagLoadTheLoadingScreenFaster then
RunService.RenderStepped:wait()
else
wait()
end
end
screenGui.Parent = CoreGui
infoFrame.RootLocalizationTable = CoreGui:FindFirstChild("CoreScriptLocalization")
currScreenGui = screenGui
local function onResized()
local isPortrait = screenGui.AbsoluteSize.X < screenGui.AbsoluteSize.Y
infoFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
infoFrame.AnchorPoint = Vector2.new(0.5, 0.5)
infoFrame.Size = UDim2.new(0.75, 0, 1, 0)
infoFrameAspect.AspectRatio = isPortrait and 2/3 or 3/2
placeLabel.Size = UDim2.new(1, 0, 0, isTenFootInterface and 120 or 80)
if isTenFootInterface then
creatorLabel.Size = UDim2.new(0, creatorLabel.TextBounds.X, 1, 0)
else
creatorLabel.Size = UDim2.new(1, 0, 0, 30)
end
infoFrameList.FillDirection = Enum.FillDirection.Vertical
infoFrameList.HorizontalAlignment = Enum.HorizontalAlignment.Center
infoFrameList.Padding = UDim.new(0, 0)
textContainer.Parent = nil
infoFrameList:ApplyLayout()
placeLabel.TextXAlignment = Enum.TextXAlignment.Center
placeLabel.AutoLocalize = false
end
onResized()
screenGui:GetPropertyChangedSignal("AbsoluteSize"):connect(onResized)
end
---------------------------------------------------------
-- Main Script (show something now + setup connections)
-- start loading assets asap
InfoProvider:LoadAssets()
GenerateGui()
if isTenFootInterface then
createTenfootCancelGui()
end
local fadeCycleTime = 1.7
local turnCycleTime = 2
local function spinnerEasingFunc(a, b, t)
t = t * 2
if t < 1 then
return b / 2 * t*t*t + a
else
t = t - 2
return b / 2 * (t * t * t + 2) + b
end
end
if FFlagLoadingScreenDontBlockOnPolicyService then
local showAntiAddictionNoticeStringEn = false
if FFlagLoadingScreenShowBlankUntilPolicyServiceReturns then
showAntiAddictionNoticeStringEn = "waiting"
end
-- PolicyService requires LocalPlayer to exist, which doesn't happen until
-- after we connect to the server. If the server is full, this can take a
-- very long time (like several minutes) to return a value. As a result, we
-- call it asynchronously and default to false.
coroutine.wrap(function()
showAntiAddictionNoticeStringEn = PolicyService:IsSubjectToChinaPolicies()
end)()
renderSteppedConnection = RunService.RenderStepped:connect(function(dt)
if not currScreenGui then return end
if not currScreenGui:FindFirstChild("BlackFrame") then return end
local infoFrame = currScreenGui.BlackFrame:FindFirstChild('InfoFrame')
if infoFrame then
-- set place name
if placeLabel and placeLabel.Text == "" then
placeLabel.Text = InfoProvider:GetGameName()
end
-- set creator name
if creatorLabel and creatorLabel.Text == "" then
if FFlagLoadingScreenShowBlankUntilPolicyServiceReturns and showAntiAddictionNoticeStringEn == "waiting" then
creatorLabel.Text = ""
elseif showAntiAddictionNoticeStringEn then
creatorLabel.Text = antiAddictionNoticeStringEn
else
local creatorName = InfoProvider:GetCreatorName()
if creatorName ~= "" then
if isTenFootInterface then
creatorLabel.Text = creatorName
creatorLabel.Size = UDim2.new(0, creatorLabel.TextBounds.X, 1, 0)
else
creatorLabel.Text = "By ".. creatorName
end
end
end
end
end
local currentTime = tick()
local fadeAmount = dt * fadeCycleTime
local spinnerImage = currScreenGui.BlackFrame.GraphicsFrame.LoadingImage
local timeInCycle = currentTime % turnCycleTime
local cycleAlpha = spinnerEasingFunc(0, 1, timeInCycle / turnCycleTime)
spinnerImage.Rotation = cycleAlpha * 360
if not isTenFootInterface then
if currentTime - startTime > 5 and currScreenGui.BlackFrame.CloseButton.ImageTransparency > 0 then
currScreenGui.BlackFrame.CloseButton.ImageTransparency = currScreenGui.BlackFrame.CloseButton.ImageTransparency - fadeAmount
if currScreenGui.BlackFrame.CloseButton.ImageTransparency <= 0 then
currScreenGui.BlackFrame.CloseButton.Active = true
end
end
end
end)
else
coroutine.wrap(function()
local showAntiAddictionNoticeStringEn = PolicyService:IsSubjectToChinaPolicies()
renderSteppedConnection = RunService.RenderStepped:connect(function(dt)
if not currScreenGui then return end
if not currScreenGui:FindFirstChild("BlackFrame") then return end
local infoFrame = currScreenGui.BlackFrame:FindFirstChild('InfoFrame')
if infoFrame then
-- set place name
if placeLabel and placeLabel.Text == "" then
placeLabel.Text = InfoProvider:GetGameName()
end
-- set creator name
if creatorLabel and creatorLabel.Text == "" then
if showAntiAddictionNoticeStringEn then
creatorLabel.Text = antiAddictionNoticeStringEn
else
local creatorName = InfoProvider:GetCreatorName()
if creatorName ~= "" then
if isTenFootInterface then
creatorLabel.Text = creatorName
creatorLabel.Size = UDim2.new(0, creatorLabel.TextBounds.X, 1, 0)
else
creatorLabel.Text = "By ".. creatorName
end
end
end
end
end
local currentTime = tick()
local fadeAmount = dt * fadeCycleTime
local spinnerImage = currScreenGui.BlackFrame.GraphicsFrame.LoadingImage
local timeInCycle = currentTime % turnCycleTime
local cycleAlpha = spinnerEasingFunc(0, 1, timeInCycle / turnCycleTime)
spinnerImage.Rotation = cycleAlpha * 360
if not isTenFootInterface then
if currentTime - startTime > 5 and currScreenGui.BlackFrame.CloseButton.ImageTransparency > 0 then
currScreenGui.BlackFrame.CloseButton.ImageTransparency = currScreenGui.BlackFrame.CloseButton.ImageTransparency - fadeAmount
if currScreenGui.BlackFrame.CloseButton.ImageTransparency <= 0 then
currScreenGui.BlackFrame.CloseButton.Active = true
end
end
end
end)
end)()
end
-- use the old error frame when on XBox
if not FFlagConnectionScriptEnabled or isTenFootInterface then
local errorImage
GuiService.ErrorMessageChanged:connect(function()
if GuiService:GetErrorMessage() ~= '' then
--TODO: Remove this reference to Utility
local utility = require(RobloxGui.Modules.Settings.Utility)
if isTenFootInterface then
currScreenGui.ErrorFrame.Size = UDim2.new(1, 0, 0, 144)
currScreenGui.ErrorFrame.Position = UDim2.new(0, 0, 0, 0)
currScreenGui.ErrorFrame.BackgroundColor3 = COLORS.BACKGROUND_COLOR
currScreenGui.ErrorFrame.BackgroundTransparency = 0.5
currScreenGui.ErrorFrame.ErrorText.TextSize = 36
currScreenGui.ErrorFrame.ErrorText.Position = UDim2.new(.3, 0, 0, 0)
currScreenGui.ErrorFrame.ErrorText.Size = UDim2.new(.4, 0, 0, 144)
if errorImage == nil then
errorImage = Instance.new("ImageLabel")
errorImage.Image = "rbxasset://textures/ui/ErrorIconSmall.png"
errorImage.Size = UDim2.new(0, 96, 0, 79)
errorImage.Position = UDim2.new(0.228125, 0, 0, 32)
errorImage.ZIndex = 9
errorImage.BackgroundTransparency = 1
errorImage.Parent = currScreenGui.ErrorFrame
end
elseif utility:IsSmallTouchScreen() then
currScreenGui.ErrorFrame.Size = UDim2.new(0.5, 0, 0, 40)
end
local errorCode = GuiService:GetErrorCode()
local errorMessage = GuiService:GetErrorMessage()
if not FFlagShowConnectionErrorCode then
currScreenGui.ErrorFrame.ErrorText.Text = errorMessage
else
if not errorCode then
currScreenGui.ErrorFrame.ErrorText.Text = ("%s (Error Code: -1)"):format(errorMessage)
else
currScreenGui.ErrorFrame.ErrorText.Text = ("%s (Error Code: %d)"):format(errorMessage, errorCode.Value)
end
end
currScreenGui.ErrorFrame.Visible = true
local blackFrame = currScreenGui:FindFirstChild('BlackFrame')
if blackFrame then
blackFrame.CloseButton.ImageTransparency = 0
blackFrame.CloseButton.Active = true
end
else
currScreenGui.ErrorFrame.Visible = false
end
end)
end
if FFlagBackButtonWhileLoadingGoesBackToApp then
backButtonConnection = GuiService.ShowLeaveConfirmation:Connect(function()
-- When OS back button is pressed during loading screen, exit
-- immediately. Behaves the same as the close button.
game:Shutdown()
end)
end
GuiService.UiMessageChanged:connect(function(type, newMessage)
if type == Enum.UiMessageType.UiMessageInfo then
local blackFrame = currScreenGui and currScreenGui:FindFirstChild('BlackFrame')
if blackFrame then
local infoFrame = blackFrame:FindFirstChild("InfoFrame")
if infoFrame then
local uiMessage = infoFrame.UiMessageFrame.UiMessage
uiMessage.Text = newMessage
if newMessage ~= '' and layoutIsReady then
uiMessage.TextTransparency = 0
else
uiMessage.TextTransparency = 1
end
end
end
end
end)
if not FFlagConnectionScriptEnabled and GuiService:GetErrorMessage() ~= '' then
currScreenGui.ErrorFrame.ErrorText.Text = GuiService:GetErrorMessage()
currScreenGui.ErrorFrame.Visible = true
end
local function stopListeningToRenderingStep()
if renderSteppedConnection then
renderSteppedConnection:disconnect()
renderSteppedConnection = nil
end
end
local function disconnectAndCloseHealthStat()
if connectionHealthCon then
connectionHealthCon:disconnect()
connectionHealthCon = nil
GuiService:CloseStatsBasedOnInputString("ConnectionHealth")
end
end
local function fadeAndDestroyBlackFrame(blackFrame)
if destroyingBackground then return end
destroyingBackground = true
spawn(function()
local infoFrame = blackFrame:FindFirstChild("InfoFrame")
local graphicsFrame = blackFrame:FindFirstChild("GraphicsFrame")
local infoFrameDescendants = infoFrame:GetDescendants()
local transparency = 0
local rateChange = 1.8
local lastUpdateTime = nil
--Notify everything else to stop messing with transparency to avoid ugly fighting effects
backgroundFadeStarted = true
if tweenPlaceIcon then
tweenPlaceIcon:Cancel()
tweenPlaceIcon = nil
end
while transparency < 1 do
RunService.RenderStepped:wait()
if not lastUpdateTime then
lastUpdateTime = tick()
else
local newTime = tick()
transparency = transparency + rateChange * (newTime - lastUpdateTime)
for i = 1, #infoFrameDescendants do
local child = infoFrameDescendants[i]
if child:IsA('TextLabel') then
child.TextTransparency = transparency
elseif child:IsA('ImageLabel') then
child.ImageTransparency = transparency
end
end
graphicsFrame.LoadingImage.ImageTransparency = transparency
blackFrame.BackgroundTransparency = transparency
lastUpdateTime = newTime
end
end
if blackFrame ~= nil then
stopListeningToRenderingStep()
blackFrame:Destroy()
end
if waitForGameIdConnection then
waitForGameIdConnection:Disconnect()
end
if loadingImageInputBeganConn then
loadingImageInputBeganConn:disconnect()
end
if backButtonConnection then
backButtonConnection:Disconnect()
end
if connectionHealthShown then
if UserInputService.TouchEnabled == true and UserInputService.MouseEnabled == false then
connectionHealthCon = game:GetService("UserInputService").InputBegan:connect(function()
disconnectAndCloseHealthStat()
end)
else
GuiService:CloseStatsBasedOnInputString("ConnectionHealth")
end
else
GuiService:CloseStatsBasedOnInputString("ConnectionHealth")
end
end)
end
local function destroyLoadingElements(instant)
if not currScreenGui then return end
if destroyedLoadingGui then return end
destroyedLoadingGui = true
local guiChildren = currScreenGui:GetChildren()
for i=1, #guiChildren do
-- need to keep this around in case we get a connection error later
if guiChildren[i].Name ~= "ErrorFrame" then
if guiChildren[i].Name == "BlackFrame" and not instant then
fadeAndDestroyBlackFrame(guiChildren[i])
else
guiChildren[i]:Destroy()
end
end
end
end
local function waitForCharacterLoaded()
if Players.CharacterAutoLoads then
local localPlayer = Players.LocalPlayer
if not localPlayer then
Players:GetPropertyChangedSignal("LocalPlayer"):wait()
localPlayer = Players.LocalPlayer
end
if not localPlayer.Character then
localPlayer.CharacterAdded:wait()
end
end
end
local function handleRemoveDefaultLoadingGui(instant)
if isTenFootInterface then
ContextActionService:UnbindCoreAction('CancelGameLoad')
end
destroyLoadingElements(instant)
ReplicatedFirst:SetDefaultLoadingGuiRemoved()
end
local function handleFinishedReplicating()
if #ReplicatedFirst:GetChildren() == 0 then
if game:IsLoaded() then
waitForCharacterLoaded()
handleRemoveDefaultLoadingGui()
else
local gameLoadedCon = nil
gameLoadedCon = game.Loaded:connect(function()
gameLoadedCon:disconnect()
gameLoadedCon = nil
waitForCharacterLoaded()
handleRemoveDefaultLoadingGui()
end)
end
else
wait(5) -- make sure after 5 seconds we remove the default gui, even if the user doesn't
handleRemoveDefaultLoadingGui()
end
end
if debugMode then
warn("Not destroying loading screen because debugMode is true")
return
end
ReplicatedFirst.FinishedReplicating:connect(handleFinishedReplicating)
if ReplicatedFirst:IsFinishedReplicating() then
handleFinishedReplicating()
end
ReplicatedFirst.RemoveDefaultLoadingGuiSignal:connect(handleRemoveDefaultLoadingGui)
if ReplicatedFirst:IsDefaultLoadingGuiRemoved() then
handleRemoveDefaultLoadingGui()
end
coroutine.wrap(function()
if not VRService.VREnabled then
VRService:GetPropertyChangedSignal("VREnabled"):Wait()
end
handleRemoveDefaultLoadingGui(true)
require(RobloxGui.Modules.LoadingScreen3D)
end)()
@@ -0,0 +1,33 @@
-- >= left < mid, >= mid <= right
function bottomupmerge(comp, a, b, left, mid, right)
local i, j = left, mid
for k = left, right do
if i < mid and (j > right or not comp(a[j], a[i])) then
b[k] = a[i]
i = i + 1
else
b[k] = a[j]
j = j + 1
end
end
end
function mergesort(arr, comp)
local work = {}
for i = 1, #arr do
work[i] = arr[i]
end
local width = 1
while width < #arr do
for i = 1, #arr, 2*width do
bottomupmerge(comp, arr, work, i, math.min(i+width, #arr), math.min(i+2*width-1, #arr))
end
local temp = work
work = arr
arr = temp
width = width * 2
end
return arr
end
return mergesort(...)
@@ -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

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