local CoreGui = game:GetService("CoreGui") local CorePackages = game:GetService("CorePackages") local TweenService = game:GetService("TweenService") local RobloxGui = CoreGui:WaitForChild("RobloxGui") local TextService = game:GetService("TextService") local GuiService = game:GetService("GuiService") local LocalizationService = game:GetService("LocalizationService") local create = require(RobloxGui.Modules.Common.Create) local MouseIconOverrideService = require(CorePackages.InGameServices.MouseIconOverrideService) local Constants = require(RobloxGui.Modules.Common.Constants) local Shimmer = require(RobloxGui.Modules.Shimmer) local fflagLocalizeErrorCodeString = settings():GetFFlag("LocalizeErrorCodeString") local DEFAULT_ERROR_PROMPT_KEY = "ErrorPrompt" -- Animation Preset -- local tweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut, 0, false, 0) -- Animation Preset -- local coreScriptTableTranslator local function onLocaleIdChanged() coreScriptTableTranslator = CoreGui.CoreScriptLocalization:GetTranslator(LocalizationService.RobloxLocaleId) end onLocaleIdChanged() LocalizationService:GetPropertyChangedSignal("RobloxLocaleId"):connect(onLocaleIdChanged) local function attemptTranslate(key, defaultString, parameters) if not coreScriptTableTranslator then return defaultString end local success,result = pcall(function() return coreScriptTableTranslator:FormatByKey(key, parameters) end) return success and result or defaultString end -- Frame styles for different platforms local styledFrame = { ["Default"] = function() return create 'Frame' { Name = 'ErrorPrompt', BackgroundTransparency = 0, BorderSizePixel = 0, BackgroundColor3 = Constants.COLORS.FLINT, AnchorPoint= Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.5, 0), Size = UDim2.new(0, Constants.ERROR_PROMPT_MAX_WIDTH.Default, 0, Constants.ERROR_PROMPT_HEIGHT.Default), Visible = false, ZIndex = 8, create 'UIListLayout' { Name = "PromptLayout", SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Center, }, create 'UIScale' { Name = "PromptScale", Scale = 0, }, create 'Frame' { Name = "TitleFrame", LayoutOrder = 1, BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, Constants.ERROR_TITLE_FRAME_HEIGHT.Default), BorderSizePixel = 0, ZIndex = 8, create 'UIPadding' { Name = "TitleFramePadding", PaddingBottom = UDim.new(0, 11), PaddingTop = UDim.new(0, 11), }, create 'TextLabel' { Name = "ErrorTitle", TextColor3 = Constants.COLORS.WHITE, TextSize = 25, Size = UDim2.new(1, 0, 0, 28), BackgroundTransparency = 1, Font = Enum.Font.SourceSansSemibold, TextXAlignment = Enum.TextXAlignment.Center, TextYAlignment = Enum.TextYAlignment.Center, ZIndex = 8, }, }, create 'Frame' { Name = "SplitLine", LayoutOrder = 2, Size = UDim2.new(1, -2 * Constants.SIDE_PADDING, 0, Constants.SPLIT_LINE_THICKNESS), BackgroundColor3 = Constants.COLORS.PUMICE, BorderSizePixel = 0, ZIndex = 8, }, create 'Frame' { Name = "MessageArea", LayoutOrder = 3, Size = UDim2.new(1, 0, 1, - Constants.ERROR_TITLE_FRAME_HEIGHT.Default - Constants.SPLIT_LINE_THICKNESS), BackgroundTransparency = 1, BorderSizePixel = 0, ZIndex = 8, create 'UIPadding' { Name = "MessageAreaPadding", PaddingBottom = UDim.new(0, Constants.SIDE_PADDING), PaddingLeft = UDim.new(0, Constants.SIDE_PADDING), PaddingRight = UDim.new(0, Constants.SIDE_PADDING), PaddingTop = UDim.new(0, Constants.SIDE_PADDING), }, create 'Frame' { Name = "ErrorFrame", BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0), ZIndex = 8, create 'UIListLayout' { Name = "ErrorFrameLayout", Padding = UDim.new(0, Constants.LAYOUT_PADDING), HorizontalAlignment = Enum.HorizontalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder, }, create 'TextLabel' { Name = "ErrorMessage", LayoutOrder = 1, Size = UDim2.new(1, 0, 1, -Constants.BUTTON_HEIGHT - Constants.LAYOUT_PADDING), TextSize = 20, TextColor3 = Constants.COLORS.PUMICE, ZIndex = 8, BackgroundTransparency = 1, TextWrapped = true, Font = Enum.Font.SourceSans, }, create 'Frame' { Name = "ButtonArea", BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, Constants.BUTTON_HEIGHT), ZIndex = 8, LayoutOrder = 2, create 'UIGridLayout' { Name = "ButtonLayout", CellPadding = UDim2.new(0, Constants.BUTTON_CELL_PADDING, 0, 0), CellSize = UDim2.new(1, 0, 0, Constants.BUTTON_HEIGHT), HorizontalAlignment = Enum.HorizontalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder, }, }, -- ButtonArea }, -- ErrorFrame }, -- MessageArea } -- ErrorPrompt end, } local styledButton = { ["Default"] = function(name, layoutOrder, primary) return create 'ImageButton' { Name = name.."Button", BackgroundTransparency = 1, ImageColor3 = primary and Constants.COLORS.WHITE or Constants.COLORS.PUMICE, AnchorPoint= Vector2.new(0.5, 0.5), Size = UDim2.new(1, 0, 1, 0), Image = primary and Constants.PRIMARY_BUTTON_TEXTURE or Constants.SECONDARY_BUTTON_TEXTURE, ScaleType = Enum.ScaleType.Slice, SliceCenter = Rect.new(8, 8, 9, 9), LayoutOrder = layoutOrder, ZIndex = 8, create 'TextLabel' { Name = "ButtonText", Text = name, Size = UDim2.new(1, 0, 1, 0), TextColor3 = primary and Constants.COLORS.SLATE or Constants.COLORS.PUMICE, BackgroundTransparency = 1, TextXAlignment = Enum.TextXAlignment.Center, TextYAlignment = Enum.TextYAlignment.Center, Font = Enum.Font.SourceSans, TextSize = 20, ZIndex = 8, } } end, } local ErrorPrompt = {} ErrorPrompt.__index = ErrorPrompt --[[ extraConfiguration: * MessageTextScaled -- Message Text Scale to fit * HideErrorCode -- If set to true, no errorCode will be appended * PlayAnimation -- Skip animation of the errorPrompt * MenuIsOpenKey -- The menu to mark opened and closed using GuiService:SetMenuIsOpen ]] function ErrorPrompt.new(style, extraConfiguration) local self = setmetatable({}, ErrorPrompt) self._frame = style and styledFrame[style]() or styledFrame["Default"]() -- prevent duplicate open/close animation if two errors occurs concurrently self._isOpen = false self._openAnimation = TweenService:Create(self._frame.PromptScale, tweenInfo, {Scale = 1}) self._closeAnimation = TweenService:Create(self._frame.PromptScale, tweenInfo, {Scale = 0}) -- re-layout when there is no button self._buttonCount = 0 self._playAnimation = true self._hideErrorCode = false self._menuIsOpenKey = DEFAULT_ERROR_PROMPT_KEY if extraConfiguration then if extraConfiguration.PlayAnimation ~= nil then self._playAnimation = extraConfiguration.PlayAnimation end if extraConfiguration.HideErrorCode ~= nil then self._hideErrorCode = extraConfiguration.HideErrorCode end if extraConfiguration.MenuIsOpenKey ~= nil then self._menuIsOpenKey = extraConfiguration.MenuIsOpenKey end local errorLabel = self._frame.MessageArea.ErrorFrame.ErrorMessage errorLabel.TextScaled = extraConfiguration.MessageTextScaled or false end return self end function ErrorPrompt:_open(errorMsg, errorCode) self:setErrorText(errorMsg, errorCode) self:resizeHeight() if not self._isOpen then MouseIconOverrideService.push("ErrorPromptOverride", Enum.OverrideMouseIconBehavior.ForceShow) GuiService:SetMenuIsOpen(true, self._menuIsOpenKey) self._frame.Visible = true self._isOpen = true if self._playAnimation then self._openAnimation:Play() self._openAnimation.Completed:wait() self._frame.PromptScale.Scale = 1 else self._frame.PromptScale.Scale = 1 end end end function ErrorPrompt:_close() if self._isOpen then MouseIconOverrideService.pop("ErrorPromptOverride") GuiService:SetMenuIsOpen(false, self._menuIsOpenKey) self._isOpen = false if self._playAnimation then self._closeAnimation:Play() self._closeAnimation.Completed:wait() else self._frame.PromptScale.Scale = 0 end self._frame.Visible = false end end function ErrorPrompt:setParent(parent) self._frame.Parent = parent end function ErrorPrompt:setErrorText(errorMsg, errorCode) -- Any unknown error that uses guiservices will have errno(UNKNOWN) as -1 local errorLabel = self._frame.MessageArea.ErrorFrame.ErrorMessage if self._hideErrorCode then errorLabel.Text = errorMsg else if fflagLocalizeErrorCodeString then local errorCodeValue = -1 if errorCode then errorCodeValue = errorCode.Value end local defaultErrorCodeString = ("Error Code: %d"):format(errorCodeValue) local localizedErrorCodeString = attemptTranslate( "InGame.ConnectionError.Message.ErrorCode", defaultErrorCodeString, {ERROR_CODE = tostring(errorCodeValue)}) errorLabel.Text = ("%s\n(%s)"):format(errorMsg,localizedErrorCodeString) else if not errorCode then errorLabel.Text = ("%s\n(Error Code: -1)"):format(errorMsg) else errorLabel.Text = ("%s\n(Error Code: %d)"):format(errorMsg, errorCode.Value) end end end end function ErrorPrompt:setErrorTitle(title, localizationKey) if not title then return end local errorTitle = self._frame.TitleFrame.ErrorTitle if localizationKey then errorTitle.Text = attemptTranslate(localizationKey, title) else errorTitle.Text = title end end function ErrorPrompt:onErrorChanged(errorMsg, errorCode) if errorMsg == "" then self:_close() elseif errorMsg ~= "" then self:_open(errorMsg, errorCode) end end -- centralize the text when there is no button enabled function ErrorPrompt:_relayout() local buttonArea = self._frame.MessageArea.ErrorFrame.ButtonArea if self._buttonCount == 0 then buttonArea.Visible = false else local gridWidth = (self._frame.Size.X.Offset - (self._buttonCount - 1) * Constants.BUTTON_CELL_PADDING - 2 * Constants.SIDE_PADDING) / self._buttonCount buttonArea.Visible = true buttonArea.ButtonLayout.CellSize = UDim2.new(0, gridWidth, 0, Constants.BUTTON_HEIGHT) end end function ErrorPrompt:clearButtons() local buttonArea = self._frame.MessageArea.ErrorFrame.ButtonArea if self._primaryShimmer then -- free previous shimmer Object self._primaryShimmer = nil end local children = buttonArea:GetChildren() for _, child in pairs(children) do if child.name ~= "ButtonLayout" then child:Destroy() end end end --[[ sample buttonlist: { relaunchButton = { Text = "Retry", LocalizationKey = "InGame.ConnectionError.Button.Retry" Callback = relaunchCallback, LayoutOrder = 1, Primary = true }, leaveButton = { Text = "Cancel", LocalizationKey = "Feature.SettingsHub.Action.CancelSearch" Callback = leaveCallback, LayoutOrder = 2, } } ]] function ErrorPrompt:updateButtons(buttonList, style) if not buttonList then buttonList = {} end if not style then style = "Default" end self:clearButtons() local buttonArea = self._frame.MessageArea.ErrorFrame.ButtonArea local buttonCount = 0 for _, buttonData in pairs(buttonList) do local buttonText = buttonData.Text if buttonData.LocalizationKey then buttonText = attemptTranslate(buttonData.LocalizationKey, buttonData.Text) end local button = styledButton[style](buttonText, buttonData.LayoutOrder, buttonData.Primary) if buttonData.Primary then self._primaryShimmer = Shimmer.new(button, "PrimaryButton") end button.Parent = buttonArea button.Activated:connect(buttonData.Callback) buttonCount = buttonCount + 1 end if self._buttonCount ~= buttonCount then self._buttonCount = buttonCount self:_relayout() end end function ErrorPrompt:primaryShimmerPlay() if self._primaryShimmer then self._primaryShimmer:play() end end function ErrorPrompt:primaryShimmerStop() if self._primaryShimmer then self._primaryShimmer:stop() end end function ErrorPrompt:resizeWidth(screenWidth) local currentWidth = self._frame.Size.X.Offset local targetWidth = screenWidth - 2 * Constants.SIDE_MARGIN if targetWidth > Constants.ERROR_PROMPT_MAX_WIDTH.Default then if currentWidth == Constants.ERROR_PROMPT_MAX_WIDTH.Default then return else targetWidth = Constants.ERROR_PROMPT_MAX_WIDTH.Default end end if targetWidth < Constants.ERROR_PROMPT_MIN_WIDTH.Default then if currentWidth == Constants.ERROR_PROMPT_MIN_WIDTH.Default then return else targetWidth = Constants.ERROR_PROMPT_MIN_WIDTH.Default end end self._frame.Size = UDim2.new(0, targetWidth, 0, self._frame.Size.Y.Offset) self:resizeHeight() self:_relayout() end function ErrorPrompt:resizeHeight() local errorTextLabel = self._frame.MessageArea.ErrorFrame.ErrorMessage local frameSize = Vector2.new(self._frame.Size.X.Offset - 2 * Constants.SIDE_PADDING, 1000) local textLabelSize = TextService:GetTextSize(errorTextLabel.Text, errorTextLabel.TextSize, errorTextLabel.Font, frameSize) -- errorMessageLabel height is the *ONLY* dynamically changing factor that could affect the prompt size -- calulate the prompt height manually to avoid circular reference for size calculation local targetHeight = Constants.ERROR_TITLE_FRAME_HEIGHT.Default + textLabelSize.Y + Constants.SPLIT_LINE_THICKNESS + Constants.BUTTON_HEIGHT + Constants.LAYOUT_PADDING + 2 * Constants.SIDE_PADDING + 1 self._frame.Size = UDim2.new(0, self._frame.Size.X.Offset, 0, targetHeight) end return ErrorPrompt