add gs
This commit is contained in:
@@ -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
|
||||
+107
@@ -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
|
||||
+227
@@ -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
|
||||
-- Don’t 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"
|
||||
}
|
||||
}
|
||||
+309
@@ -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()
|
||||
+442
@@ -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
|
||||
+311
@@ -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()
|
||||
+361
@@ -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
|
||||
+118
@@ -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()
|
||||
+105
@@ -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()
|
||||
+9
@@ -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)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function()
|
||||
return {}
|
||||
end)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Action = require(CorePackages.AppTempCommon.Common.Action)
|
||||
|
||||
return Action(script.Name, function()
|
||||
return {}
|
||||
end)
|
||||
+9
@@ -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)
|
||||
+10
@@ -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)
|
||||
+9
@@ -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)
|
||||
+171
@@ -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)
|
||||
+79
@@ -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)
|
||||
+106
@@ -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)
|
||||
+17
@@ -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
|
||||
+300
@@ -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
|
||||
+85
@@ -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
|
||||
+33
@@ -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
|
||||
+94
@@ -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
|
||||
+38
@@ -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
|
||||
+370
@@ -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)
|
||||
+78
@@ -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)
|
||||
+46
@@ -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
|
||||
+209
@@ -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)
|
||||
+51
@@ -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
|
||||
+218
@@ -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)
|
||||
+51
@@ -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
|
||||
+260
@@ -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)
|
||||
+55
@@ -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
|
||||
+128
@@ -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)
|
||||
+51
@@ -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
|
||||
+1
@@ -0,0 +1 @@
|
||||
return game:DefineFastFlag("MakeGetAssetsDifferenceFaster", false)
|
||||
+67
@@ -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
|
||||
+48
@@ -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
|
||||
+110
@@ -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
|
||||
+80
@@ -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
|
||||
+18
@@ -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
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
local enumerate = require(CorePackages.enumerate)
|
||||
|
||||
return enumerate("PromptType", {
|
||||
"AllowInventoryReadAccess",
|
||||
"SaveAvatar",
|
||||
"CreateOutfit",
|
||||
"EnterOutfitName",
|
||||
"SetFavorite",
|
||||
})
|
||||
+27
@@ -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
|
||||
+19
@@ -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,
|
||||
})
|
||||
+67
@@ -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
|
||||
+190
@@ -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
|
||||
+16
@@ -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
|
||||
+21
@@ -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
|
||||
+17
@@ -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
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
return {
|
||||
propValidation = false,
|
||||
elementTracing = false,
|
||||
}
|
||||
+22
@@ -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
|
||||
+40
@@ -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
|
||||
+13
@@ -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
|
||||
+42
@@ -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
|
||||
+27
@@ -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
|
||||
+34
@@ -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
|
||||
+10
@@ -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
|
||||
+12
@@ -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
|
||||
+10
@@ -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
|
||||
+10
@@ -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
|
||||
+10
@@ -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()
|
||||
+6
@@ -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
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
game:DefineFastFlag("FixDialogServerWait", false)
|
||||
|
||||
return function()
|
||||
return game:GetFastFlag("FixDialogServerWait")
|
||||
end
|
||||
+12
@@ -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
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
-- This Flag is a descendant of the Common folder instead of Modules/Flags
|
||||
-- because it needs to be accessible by both Modules/Common/LegacyThumbnailUrls and
|
||||
-- other places like Emotes, InspectAndBuy
|
||||
--
|
||||
-- Since the server only has access to the Common and Server folders, it's
|
||||
-- placed here so both parts of the codebase can access it.
|
||||
|
||||
game:DefineFastFlag("UseThumbnailUrl", false)
|
||||
|
||||
return function()
|
||||
return game:GetFastFlag("UseThumbnailUrl")
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user