This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,48 @@
stds.roblox = {
globals = {
"game"
},
read_globals = {
-- Roblox globals
"script",
"utf8",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof",
"UserSettings",
-- Types
"Vector2", "Vector3",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
"TweenInfo",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
"421", -- shadowing local variable
"422", -- shadowing argument
"431", -- shadowing upvalue
"432", -- shadowing upvalue argument
}
std = "lua51+roblox"
files["**/*.spec.lua"] = {
std = "+testez",
}
@@ -0,0 +1,247 @@
local RobloxGui = game:GetService("CoreGui").RobloxGui
local ShellModules = RobloxGui.Modules.Shell
local ContextActionService = game:GetService("ContextActionService")
local AccountManager = require(ShellModules.AccountManager)
local AssetManager = require(ShellModules.AssetManager)
local Errors = require(ShellModules.Errors)
local ErrorOverlay = require(ShellModules.ErrorOverlay)
local GlobalSettings = require(ShellModules.GlobalSettings)
local LoadingWidget = require(ShellModules.LoadingWidget)
local ScreenManager = require(ShellModules.ScreenManager)
local SoundManager = require(ShellModules.SoundManager)
local Strings = require(ShellModules.LocalizedStrings)
local ThumbnailLoader = require(ShellModules.ThumbnailLoader)
local UnlinkAccountOverlay = require(ShellModules.UnlinkAccountOverlay)
local Utility = require(ShellModules.Utility)
local XboxAppState = require(ShellModules.AppState)
local function createAccountLinkingView()
local this = {}
local gamerTag = XboxAppState.store:getState().XboxUser.gamertag
local robloxName = XboxAppState.store:getState().RobloxUser.robloxName
local rbxuid = XboxAppState.store:getState().RobloxUser.rbxuid
local linkedAsPhrase = string.format(Strings:LocalizedString("LinkedAsPhrase"), gamerTag, robloxName)
local dummySelection = Utility.Create"Frame" {
BackgroundTransparency = 1,
}
local Container = Utility.Create"Frame" {
Name = "Container",
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(0, 765, 0, 630),
BackgroundTransparency = 1,
BorderSizePixel = 0,
Selectable = true,
SelectionImageObject = dummySelection,
}
Utility.Create"ImageLabel" {
Name = "GamerPic",
Position = UDim2.new(0, 40, 0, 25),
Size = UDim2.new(0, 300, 0, 300),
BackgroundTransparency = 0,
BorderSizePixel = 0,
Image = "rbxapp://xbox/localgamerpic",
Parent = Container,
}
Utility.Create"ImageLabel" {
Name = "AccountLinkIcon",
Position = UDim2.new(0, 354, 0, 166),
Size = UDim2.new(0, 58, 0, 20),
BackgroundTransparency = 1,
Image = "rbxasset://textures/ui/Shell/Icons/AccountLinkIcon.png",
Parent = Container,
}
local ProfileImage = Utility.Create"ImageLabel" {
Name = "ProfileImage",
Position = UDim2.new(0, 425, 0, 25),
Size = UDim2.new(0, 300, 0, 300),
BackgroundTransparency = 0,
BackgroundColor3 = GlobalSettings.CharacterBackgroundColor,
BorderSizePixel = 0,
Parent = Container,
}
if rbxuid then
spawn(function()
local thumbnailSize = ThumbnailLoader.AvatarSizes.Size352x352
local thumbLoader = ThumbnailLoader:LoadAvatarThumbnailAsync(ProfileImage, rbxuid,
Enum.ThumbnailType.AvatarThumbnail, Enum.ThumbnailSize.Size352x352, true)
thumbLoader:LoadAsync()
ProfileImage.ImageRectSize = Vector2.new(thumbnailSize.X, (1) * thumbnailSize.X)
end)
end
Utility.Create"TextLabel" {
Name = "ProfileLabel",
Position = UDim2.new(0, 575, 0, 335),
Text = robloxName or "",
TextXAlignment = "Center",
TextYAlignment = "Top",
BackgroundColor3 = Color3.new(1, 0, 0),
TextColor3 = GlobalSettings.WhiteTextColor,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.SubHeaderSize,
BackgroundTransparency = 1,
Parent = Container,
}
Utility.Create"TextLabel" {
Name = "GamerLabel",
Text = gamerTag or "",
TextXAlignment = "Center",
TextYAlignment = "Top",
Position = UDim2.new(0, 190, 0, 335),
BackgroundColor3 = Color3.new(1,0,0),
TextColor3 = GlobalSettings.WhiteTextColor,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.SubHeaderSize,
BackgroundTransparency = 1,
Parent = Container,
}
Utility.Create"TextLabel" {
Name = "LinkedAsText",
Position = UDim2.new(0, 40, 0, 395),
Size = UDim2.new(0, 686, 0, 120),
Text = linkedAsPhrase,
TextXAlignment = "Center",
TextYAlignment = "Top",
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
TextColor3 = GlobalSettings.GreyTextColor,
FontSize = GlobalSettings.SubHeaderSize,
TextWrapped = true,
Parent = Container,
}
local UnlinkButton = Utility.Create"ImageButton" {
Name = "UnlinkButton",
Position = UDim2.new(0, 220, 0, 520),
Size = UDim2.new(0, 320, 0, 80),
BackgroundTransparency = 1,
ImageColor3 = GlobalSettings.GreySelectedButtonColor,
Image = GlobalSettings.RoundCornerButtonImage,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28)),
ZIndex = 2,
Parent = Container,
SoundManager:CreateSound("MoveSelection"),
AssetManager.CreateShadow(1),
}
local DefaultButtonColor = GlobalSettings.GreyButtonColor
local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor
local DefaultButtonTextColor = GlobalSettings.WhiteTextColor
local SelectedButtonTextColor = GlobalSettings.TextSelectedColor
local UnlinkText = Utility.Create"TextLabel" {
Name = "UnlinkText",
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.ButtonSize,
TextColor3 = Color3.new(0, 0, 0),
Text = Strings:LocalizedString("UnlinkGamerTagWord"),
ZIndex = 2,
Parent = UnlinkButton,
}
Utility.ResizeButtonWithText(UnlinkButton, UnlinkText, GlobalSettings.TextHorizontalPadding)
Container.SelectionGained:connect(function()
Utility.SetSelectedCoreObject(UnlinkButton)
end)
UnlinkButton.SelectionGained:connect(function()
UnlinkButton.ImageColor3 = SelectedButtonColor
UnlinkText.TextColor3 = SelectedButtonTextColor
end)
UnlinkButton.SelectionLost:connect(function()
UnlinkButton.ImageColor3 = DefaultButtonColor
UnlinkText.TextColor3 = DefaultButtonTextColor
end)
local ModalOverlay = Utility.Create"Frame" {
Name = "ModalOverlay",
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = GlobalSettings.ModalBackgroundTransparency,
BackgroundColor3 = GlobalSettings.ModalBackgroundColor,
BorderSizePixel = 0,
ZIndex = 4,
}
local isUnlinking = false
local function unlinkAccountAsync()
if isUnlinking then return end
isUnlinking = true
local unlinkResult = nil
local loader = LoadingWidget(
{ Parent = Container }, {
function()
unlinkResult = AccountManager:UnlinkAccountAsync()
end
})
-- set up full screen loader
ModalOverlay.Parent = RobloxGui
ContextActionService:BindCoreAction("BlockB", function() end, false, Enum.KeyCode.ButtonB)
UnlinkButton.SelectionImageObject = dummySelection
UnlinkButton.ImageColor3 = GlobalSettings.GreyButtonColor
UnlinkText.TextColor3 = GlobalSettings.WhiteTextColor
-- call loader
loader:AwaitFinished()
-- clean up
-- NOTE: Unlink success will fire the ThirdPartyUserService ActiveUserSignedOut event.
-- This event will fire and listeners will run before the loader is finished. The below
-- code needs to run in case of errors, but on success will not interfere with the reauth
-- logic in AppHome.lua
loader:Cleanup()
UnlinkButton.SelectionImageObject = nil
UnlinkButton.ImageColor3 = GlobalSettings.GreySelectedButtonColor
UnlinkText.TextColor3 = GlobalSettings.TextSelectedColor
ContextActionService:UnbindCoreAction("BlockB")
ModalOverlay.Parent = nil
if unlinkResult ~= AccountManager.AuthResults.Success then
local err = unlinkResult and Errors.Authentication[unlinkResult] or Errors.Default
ScreenManager:OpenScreen(ErrorOverlay(err), false)
end
isUnlinking = false
end
function this:Focus()
end
function this:RemoveFocus()
end
UnlinkButton.MouseButton1Click:connect(function()
if isUnlinking then return end
SoundManager:Play("ButtonPress")
local confirmTitleAndMsg = { Title = Strings:LocalizedString("UnlinkTitle"),
Msg = Strings:LocalizedString("UnlinkPhrase") }
ScreenManager:OpenScreen(UnlinkAccountOverlay(confirmTitleAndMsg, unlinkAccountAsync), false)
end)
--[[ Public API ]]--
function this:SetParent(newParent)
Container.Parent = newParent
end
function this:GetUnlinkButton()
return UnlinkButton
end
return this
end
return createAccountLinkingView
@@ -0,0 +1,225 @@
--[[
// AccountManager.lua
// Handles all account related functions
]]
local IsNewUsernameCheckEnabled = settings():GetFFlag("XboxUseNewValidUsernameCheck2")
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local UserInputService = game:GetService('UserInputService')
local AnalyticsService = game:GetService("AnalyticsService")
local Http = require(ShellModules:FindFirstChild('Http'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local AccountManager = {}
AccountManager.AuthResults = {
Error = -1;
Success = 0;
InProgress = 1;
AccountUnlinked = 2;
MissingGamePad = 3;
NoUserDetected = 4;
HttpErrorDetected = 5;
SignUpDisabled = 6;
Flooded = 7;
LeaseLocked = 8;
AccountLinkingDisabled = 9;
InvalidRobloxUser = 10;
RobloxUserAlreadyLinked = 11;
XboxUserAlreadyLinked = 12;
IllgealChildAccountLinking = 13;
InvalidPassword = 14;
UsernamePasswordNotSet = 15;
UsernameAlreadyTaken = 16;
SiteMaintenance = 19,
}
AccountManager.InvalidUsernameReasons = {
Valid = "Valid";
InvalidUsername = "Invalid Username";
AlreadyTaken = "Already Taken";
InvalidCharactersUsed = "Invalid Characters Used";
UsernameCannotContainSpaces = "Username Cannot Contain Spaces";
}
--[[ Authentication ]]--
local function authenticateStudio()
return AccountManager.AuthResults.Success
end
--[[ Signup/Login ]]--
function AccountManager:LoginAsync()
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
return authenticateStudio()
end
local success, result = pcall(function()
return PlatformService:BeginPlatformLogin()
end)
-- catch pcall failure, something went wrong with API call
if not success then
return self.AuthResults.Error
end
return result
end
function AccountManager:SignupAsync(username, password)
local success, result = pcall(function()
return PlatformService:BeginPlatformSignup(username, password)
end)
-- catch pcall failure, something went wrong with the API call
if not success then
result = self.AuthResults.Error
end
if result == self.AuthResults.Success then
AnalyticsService:ReportCounter("Xbox_SignUp_New_Account_Success")
AnalyticsService:ReportCounter("Xbox_SignUp_Success")
end
return result
end
--[[ Account Linking ]]--
-- called at sign in
function AccountManager:LinkAccountAsync(accountName, password)
local success, result = pcall(function()
-- PlatformService may not exist on studio platform
return PlatformService:BeginAccountLink(accountName, password)
end)
if not success then
Utility.DebugLog("AccountManager:LinkAccountAsync() failed because", result)
result = AccountManager.AuthResults.Error
end
if result == self.AuthResults.Success then
AnalyticsService:ReportCounter("Xbox_SignUp_Account_Link_Success")
AnalyticsService:ReportCounter("Xbox_SignUp_Success")
end
return result
end
-- used when setting credentials for a generated account
function AccountManager:SetRobloxCredentialsAsync(accountName, password)
local success, result = pcall(function()
-- PlatformService may not exist on studio platform
return PlatformService:BeginSetRobloxCredentials(accountName, password)
end)
if not success then
Utility.DebugLog("AccountManager:SetRobloxCredentialsAsync() failed because", result)
result = AccountManager.AuthResults.Error
end
return result
end
-- called when user has roblox credentials
function AccountManager:UnlinkAccountAsync()
local success, result = pcall(function()
-- PlatformService may not exist on studio platform
return PlatformService:BeginUnlinkAccount()
end)
if not success then
Utility.DebugLog("AccountManager:UnlinkAccountAsync() failed because", result)
result = AccountManager.AuthResults.Error
end
return result
end
-- Do NOT do this anywhere else. This is a special case for has-linked-account when we are under maintenance.
-- has-linked-account is the first endpoint we hit when authenticating
-- Puts a 15 sec throttle on request to has-linked-account during maintenance.
local isThrottled = false
local function setMaintenanceThrottle()
isThrottled = true
delay(15, function()
isThrottled = false
end)
end
function AccountManager:HasLinkedAccountAsync()
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
return AccountManager.AuthResults.Success
end
if isThrottled == true then
return AccountManager.AuthResults.SiteMaintenance
end
local success, result = pcall(function()
-- PlatformService may not exist on studio platform
return PlatformService:BeginHasLinkedAccount()
end)
if not success then
Utility.DebugLog("AccountManager:HasLinkedAccountAsync() failed because", result)
result = AccountManager.AuthResults.Error
end
if result == AccountManager.AuthResults.SiteMaintenance then
setMaintenanceThrottle()
end
return result
end
function AccountManager:HasRobloxCredentialsAsync()
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
return AccountManager.AuthResults.Success
end
local success, result = pcall(function()
-- PlatformService may not exist on studio platform
return PlatformService:BeginHasRobloxCredentials()
end)
if not success then
Utility.DebugLog("AccountManager:HasRobloxCredentialsAsync() failed because", result)
result = AccountManager.AuthResults.Error
end
return result
end
function AccountManager:IsValidUsernameAsync(username)
local result = Http.IsValidUsername(username)
if not result then
-- return false
return nil
end
-- there are two endpoints being used based on flag, they return different casing, check for both
-- old returns IsValid and ErrorMessage
-- new returns isValid and errorMessage
if IsNewUsernameCheckEnabled then
return result["isValid"], result["errorMessage"]
else
return result["IsValid"], result["ErrorMessage"]
end
return nil
end
function AccountManager:IsValidPasswordAsync(username, password)
local result = Http.IsValidPassword(username, password)
if not result then
-- return false
return nil
end
return result["IsValid"], result["ErrorMessage"]
end
return AccountManager
@@ -0,0 +1,84 @@
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local AccountLinkingView = require(ShellModules:FindFirstChild('AccountLinkingView'))
local GameplaySettingsView = require(ShellModules:FindFirstChild('GameplaySettingsView'))
local function createAccountScreen(errorCode)
local this = BaseScreen()
this:SetTitle(Strings:LocalizedString("AccountSettingsTitle"))
local AccountLinkingViewContainer = Utility.Create'Frame'
{
Name = "AccountLinkingViewContainer";
Position = UDim2.new(0, 75, 0, 275);
Size = UDim2.new(0, 765, 0, 630);
BorderSizePixel = 0;
BackgroundTransparency = 1;
Parent = this.Container
}
local accountLinkingView = AccountLinkingView()
accountLinkingView:SetParent(AccountLinkingViewContainer)
local ScreenDivide = Utility.Create'Frame'
{
Name = "ScreenDivide";
Size = UDim2.new(0, 2, 0, 615);
Position = UDim2.new(0, 840, 0, 275);
BorderSizePixel = 0;
BackgroundColor3 = GlobalSettings.PageDivideColor;
Parent = this.Container;
}
local gameplaySettingsViewContainer = Utility.Create'Frame'
{
Name = "GameplaySettingsViewContainer";
Position = UDim2.new(0, 840, 0, 275);
Size = UDim2.new(0, 765, 0, 630);
BorderSizePixel = 0;
BackgroundTransparency = 1;
Parent = this.Container;
}
local gameplaySettingsView = GameplaySettingsView(errorCode)
gameplaySettingsView:SetParent(gameplaySettingsViewContainer)
--[[ Public API ]]--
function this:GetAnalyticsInfo()
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('UnlinkAccountScreenId')}
end
-- Override
function this:GetDefaultSelectionObject()
return accountLinkingView:GetUnlinkButton()
end
-- Override
local baseFocus = this.Focus
function this:Focus()
baseFocus(self)
accountLinkingView:Focus()
gameplaySettingsView:Focus()
end
-- Override
local baseRemoveFocus = this.RemoveFocus
function this:RemoveFocus()
baseRemoveFocus(self)
accountLinkingView:RemoveFocus()
end
return this
end
return createAccountScreen
@@ -0,0 +1,388 @@
-- Written by Kip Turner, Copyright Roblox 2015
-- Achievement Manager
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local ThirdPartyUserService = nil
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
local Http = require(ShellModules:FindFirstChild('Http'))
local UserData = require(ShellModules:FindFirstChild('UserData'))
local PlatformInterface = require(ShellModules:FindFirstChild('PlatformInterface'))
local Alerts = require(ShellModules:FindFirstChild('Alerts'))
local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local SortsData = require(ShellModules:FindFirstChild('SortsData'))
local XboxAppState = require(ShellModules:FindFirstChild('AppState'))
--[[ ACHIEVEMENT NAMES --]]
-- "Award10DayRoll"
-- "Award20DayRoll"
-- "Award3DayRoll"
-- "AwardDeepDiver"
-- "AwardFoursCompany"
-- "AwardOneNameManyFaces"
-- "AwardPollster"
-- "AwardSampler"
-- "AwardStrengthInNumbers"
-- "AwardWorldTraveler"
-- "AwardYouDidIt"
-- "GameProgress"
-- "MultiplayerRoundEnd"
-- "MultiplayerRoundStart"
-- "PlayerSessionEnd"
-- "PlayerSessionPause"
-- "PlayerSessionResume"
-- "PlayerSessionStart"
-- "Test_XPresses"
--[[ END OF ACHIEVEMENT NAMES --]]
local VIEW_GAMETYPE_ENUM =
{
AppShell = 0;
Game = 1;
}
local GAMES_FOR_YOU_DID_IT = 1
local GAMES_FOR_AWARD_SAMPLER = 5
local DAYS_FOR_3DAYROLL = 3
local DAYS_FOR_10DAYROLL = 10
local DAYS_FOR_20DAYROLL = 20
local GAMES_RATED_FOR_POLLSTER = 5
local PLAY_SECONDS_FOR_DEEP_DIVER = 60 * 60
local NUMBER_OF_FRIENDS_REQUIRED_FOR_FOURS_COMPANY = 3
local SECONDS_BETWEEN_FOURS_COMPANY_CHECKS = 30
local AchievementManager = {}
local CurrentView = VIEW_GAMETYPE_ENUM['AppShell']
local partyUpdateConn = nil
AchievementManager.AchivementId = {
Scout = "2";
Explorer = "3";
Trailblazer = "4";
Pollster = "5";
Marathon = "6";
OneNameManyFaces = "7";
ThreeDayRoll = "8";
TenDayRoll = "9";
TwentyDayRoll = "10";
StrengthInNumbers = "11";
FoursCompany = "12";
}
-- Map ID to trigger name so we can do a look up in SessionAchievementState
local AchievementIdToTrigger = {
["2"] = "AwardYouDidIt";
["3"] = "AwardSampler";
["4"] = "AwardWorldTraveler";
["5"] = "AwardPollster";
["6"] = "AwardDeepDiver";
["7"] = "AwardOneNameManyFaces";
["8"] = "Award3DayRoll";
["9"] = "Award10DayRoll";
["10"] = "Award20DayRoll";
["11"] = "AwardStrengthInNumbers";
["12"] = "AwardFoursCompany";
}
local SessionAchievementState = {}
local function GetTotalNumberOfGamesOnXbox()
-- TODO: is there a programmatic way of figuring this out?
return 15
end
local function FilterInGameFriends(onlineFriends, playersInGame)
local result = {}
if onlineFriends and playersInGame then
-- Create reverse lookup for speed
local playersInGameReverseLookup = {}
for _, playerInGame in pairs(playersInGame) do
-- TODO: Figure out what the actual lookup
if playerInGame['robloxuid'] then
playersInGameReverseLookup[playerInGame['robloxuid']] = true
end
end
for _, friend in pairs(onlineFriends) do
if playersInGameReverseLookup[friend['robloxuid']] then
table.insert(result, friend)
end
end
end
return result
end
local function OnPlayedGamesChanged()
EventHub:dispatchEvent(EventHub.Notifications["PlayedGamesChanged"])
spawn(function()
local myUserId = XboxAppState.store:getState().RobloxUser.rbxuid
if myUserId then
local recentCollection
-- TODO: is this the right way of getting num of played games?
local recentlyPage1
recentCollection = SortsData:GetUserRecent()
recentlyPage1 = recentCollection and recentCollection:GetSortAsync(0, GetTotalNumberOfGamesOnXbox())
local gamesPlayed = recentlyPage1 and #recentlyPage1 or 0
Utility.DebugLog("You have played:" , gamesPlayed , "games" )
if gamesPlayed >= GAMES_FOR_YOU_DID_IT then
AchievementManager:SendAchievementEventAsync("AwardYouDidIt")
end
local hasExplorerAchievement = AchievementManager:HasAchievementAsync(AchievementManager.AchivementId.Explorer)
if gamesPlayed >= GAMES_FOR_AWARD_SAMPLER then
AchievementManager:SendAchievementEventAsync("AwardSampler")
-- if we didn't have it before then let's unlock UGC
if not hasExplorerAchievement then
EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], true)
else
EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], false)
end
else
--if the user has the ExplorerAchievement but hasn't played 5 games
--(may happen when user links a new Roblox account to the Xbox account which already has the ExplorerAchievement)
if hasExplorerAchievement then
EventHub:dispatchEvent(EventHub.Notifications["UnlockedUGC"], false)
end
end
if gamesPlayed >= GetTotalNumberOfGamesOnXbox() then
AchievementManager:SendAchievementEventAsync("AwardWorldTraveler")
end
end
end)
end
local function OnJoinedGame()
spawn(function()
Utility.DebugLog("OnJoinGame: Fours Company check")
if PlatformService then
local lastCheck = 0
while CurrentView == VIEW_GAMETYPE_ENUM['Game'] do
local now = tick()
if now - lastCheck > SECONDS_BETWEEN_FOURS_COMPANY_CHECKS then
local friendsData = require(ShellModules:FindFirstChild('FriendsData'))
local onlineFriends = friendsData.GetOnlineFriendsAsync()
-- TODO: add actually API
local inGamePlayers = PlatformService:GetInGamePlayers()
if inGamePlayers and onlineFriends then
local inGameFriends = FilterInGameFriends(onlineFriends, inGamePlayers)
if #inGameFriends >= NUMBER_OF_FRIENDS_REQUIRED_FOR_FOURS_COMPANY then
AchievementManager:SendAchievementEventAsync("AwardFoursCompany")
return
end
end
lastCheck = now
end
wait(1)
end
end
end)
spawn(function()
local startTime = tick()
while tick() - startTime < PLAY_SECONDS_FOR_DEEP_DIVER do
if CurrentView ~= VIEW_GAMETYPE_ENUM['Game'] then
return
end
wait(1)
end
if CurrentView == VIEW_GAMETYPE_ENUM['Game'] then
AchievementManager:SendAchievementEventAsync("AwardDeepDiver")
end
end)
end
local function CheckStrengthInNumbers()
Utility.DebugLog("PartyTitlePresenceChanged: AwardStrengthInNumbers check")
local partyMembers = PlatformInterface:GetPartyMembersAsync()
if partyMembers then
if PlatformInterface:IsInAParty(partyMembers) then
AchievementManager:SendAchievementEventAsync("AwardStrengthInNumbers")
if partyUpdateConn then
partyUpdateConn:disconnect()
partyUpdateConn = nil
end
end
end
end
function AchievementManager:SendAchievementEventAsync(achievementName)
-- Always set session state to awarded, we don't want to block session related achievement progress (UGC for example)
-- see: https://forums.xboxlive.com/articles/56661/achievements-and-when-they-arent-unlocking-1.html
-- there is an issue with xbox live granting achievements
SessionAchievementState[achievementName] = true
Utility.DebugLog("Achievement Manager - Awarding achievement:" , achievementName)
local achievementStatus = nil
local success, msg = pcall(function()
-- NOTE: Yielding function
if not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then
achievementStatus = PlatformService:BeginAwardAchievement(achievementName)
end
end)
if not success then
-- NOTE: very likely this function ever throws an error but returns error codes
Utility.DebugLog("Achievement Manager - Unable to award achievement:" , achievementName , "for reason:" , msg)
end
Utility.DebugLog("Achievement Manager - Achievement:" , achievementName , "event status:" , achievementStatus)
end
EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "ShowUnlockedUGCOverlay",
function(ShowAlert)
if ShowAlert == true then
ScreenManager:OpenScreen(ErrorOverlay(Alerts.UnlockedUGC), false)
end
end)
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "AchievementManager",
function()
spawn(function()
local myUserId = XboxAppState.store:getState().RobloxUser.rbxuid
local function stillLoggedIn()
local newUserId = XboxAppState.store:getState().RobloxUser.rbxuid
return newUserId ~= nil and myUserId == newUserId
end
if myUserId ~= nil then
local loggedInResult = Http.GetConsecutiveDaysLoggedInAsync()
local daysLoggedIn = loggedInResult and loggedInResult['count']
if daysLoggedIn then
if daysLoggedIn >= DAYS_FOR_3DAYROLL and stillLoggedIn() then
AchievementManager:SendAchievementEventAsync("Award3DayRoll")
end
if daysLoggedIn >= DAYS_FOR_10DAYROLL and stillLoggedIn() then
AchievementManager:SendAchievementEventAsync("Award10DayRoll")
end
if daysLoggedIn >= DAYS_FOR_20DAYROLL and stillLoggedIn() then
AchievementManager:SendAchievementEventAsync("Award20DayRoll")
end
end
--Check if using new "StrengthInNumbers" achievement implementation
if PlatformService then
local hasStrengthInNumbersAchievement = AchievementManager:HasAchievementAsync(AchievementManager.AchivementId.StrengthInNumbers)
if not hasStrengthInNumbersAchievement then
--Haven't got "StrengthInNumbers" achievement
partyUpdateConn = PlatformService.PartyTitlePresenceChanged:connect(CheckStrengthInNumbers)
CheckStrengthInNumbers()
end
end
OnPlayedGamesChanged()
end
end)
end)
EventHub:addEventListener(EventHub.Notifications["DonnedDifferentPackage"], "AchievementManager",
function(assetId)
AchievementManager:SendAchievementEventAsync("AwardOneNameManyFaces")
end)
EventHub:addEventListener(EventHub.Notifications["AvatarEquipSuccess"], "AchievementManager",
function(assetId)
AchievementManager:SendAchievementEventAsync("AwardOneNameManyFaces")
end)
EventHub:addEventListener(EventHub.Notifications["VotedOnPlace"], "AchievementManager",
function()
spawn(function()
local voteCount = UserData:GetVoteCount()
Utility.DebugLog("Vote Check: with vote count" , voteCount)
if voteCount >= GAMES_RATED_FOR_POLLSTER then
AchievementManager:SendAchievementEventAsync("AwardPollster")
end
end)
end)
if PlatformService then
PlatformService.ViewChanged:connect(function(newView)
Utility.DebugLog("ViewChanged:" , newView)
CurrentView = newView
if newView == VIEW_GAMETYPE_ENUM['AppShell'] then
Utility.DebugLog("New view is appshell")
OnPlayedGamesChanged()
elseif newView == VIEW_GAMETYPE_ENUM['Game'] then
Utility.DebugLog("New view is game")
OnJoinedGame()
end
end)
end
local UserChangedCount = 0
if ThirdPartyUserService then
ThirdPartyUserService.ActiveUserSignedOut:connect(function()
UserChangedCount = UserChangedCount + 1
SessionAchievementState = {}
end)
end
function AchievementManager:HasAchievementAsync(achievementId)
local startCount = UserChangedCount
if UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows then
SessionAchievementState[AchievementIdToTrigger[achievementId]] = true
return true
end
if SessionAchievementState[AchievementIdToTrigger[achievementId]] == true then
return true
end
local success, result = pcall(function()
return PlatformService:BeginHasAchievement(achievementId)
end)
if not success then
return false
end
--if the result is true, we use it to update SessionAchievementState
if startCount == UserChangedCount then
if result == true then
SessionAchievementState[AchievementIdToTrigger[achievementId]] = result
end
end
--[[
If the result is false or the API fails, we need to use our session state as backup. There is an edge case where locally we
grant the achievement, but xbox does not update/grant the achievement for some period of time (like xbox services being down).
We want to avoid ever locking the user from UGC if they have in fact unlocked it.
Two other cases remain. During the retro check, if xbox services are down, but we have in fact unlocked UGC because we've played
5 games, we still unlock. The user will get the notification again however.
If the user unlocks UGC then unlinks their account and creates a new Roblox account, and if xbox services are down, since we
check games played, UGC will be locked for them. It will remain locked until they play 5 games on the new account, or until
xbox services come back up and we can correctly get the state of the achievement. Dan says this is both of these are OK.
]]
return result or SessionAchievementState[AchievementIdToTrigger[achievementId]]
end
function AchievementManager:AllGamesUnlocked()
local achievementId = AchievementManager.AchivementId.Explorer
return SessionAchievementState[AchievementIdToTrigger[achievementId]]
end
return AchievementManager
@@ -0,0 +1,14 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("AddError", function(error, timestamp)
error = error or {}
return {
error = {
Title = error.Title,
Msg = error.Msg,
Code = error.Code,
timestamp = timestamp
}
}
end)
@@ -0,0 +1,39 @@
return function()
it("should return a table", function()
local action = require(script.Parent.AddError)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.AddError)()
expect(action).to.be.a("table")
end)
it("should return a table with an error with same Title, Msg, Code set as the passed in error and aslo has the error timestamp appended", function()
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local ShellModules = Modules:FindFirstChild("Shell")
local Errors = require(ShellModules:FindFirstChild('Errors'))
local DefaultError = Errors.Default
local action = require(script.Parent.AddError)(DefaultError, tick())
local error = action.error
expect(error).to.be.a("table")
expect(error.Title).to.equal(DefaultError.Title)
expect(error.Msg).to.equal(DefaultError.Msg)
expect(error.Code).to.equal(DefaultError.Code)
expect(error.timestamp).to.be.a("number")
end)
it("should set the name", function()
local action = require(script.Parent.AddError)
expect(action.name).to.equal("AddError")
end)
it("should set the type", function()
local action = require(script.Parent.AddError)()
expect(action.type).to.equal("AddError")
end)
end
@@ -0,0 +1,13 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("DeleteError", function(error)
error = error or {}
return {
error = {
Title = error.Title,
Msg = error.Msg,
Code = error.Code
}
}
end)
@@ -0,0 +1,38 @@
return function()
it("should return a table", function()
local action = require(script.Parent.DeleteError)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.DeleteError)()
expect(action).to.be.a("table")
end)
it("should return a table with an error with same Title, Msg, Code set as the passed in error", function()
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local ShellModules = Modules:FindFirstChild("Shell")
local Errors = require(ShellModules:FindFirstChild('Errors'))
local DefaultError = Errors.Default
local action = require(script.Parent.DeleteError)(DefaultError)
local error = action.error
expect(error).to.be.a("table")
expect(error.Title).to.equal(DefaultError.Title)
expect(error.Msg).to.equal(DefaultError.Msg)
expect(error.Code).to.equal(DefaultError.Code)
end)
it("should set the name", function()
local action = require(script.Parent.DeleteError)
expect(action.name).to.equal("DeleteError")
end)
it("should set the type", function()
local action = require(script.Parent.DeleteError)()
expect(action.type).to.equal("DeleteError")
end)
end
@@ -0,0 +1,6 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("FetchPrivilegeSettings", function()
return {}
end)
@@ -0,0 +1,25 @@
return function()
it("should return a table", function()
local action = require(script.Parent.FetchPrivilegeSettings)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.FetchPrivilegeSettings)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.FetchPrivilegeSettings)
expect(action.name).to.equal("FetchPrivilegeSettings")
end)
it("should set the type", function()
local action = require(script.Parent.FetchPrivilegeSettings)()
expect(action.type).to.equal("FetchPrivilegeSettings")
end)
end
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("FetchUserThumbnail", function(thumbnailInfo)
thumbnailInfo = thumbnailInfo or {}
return {
rbxuid = thumbnailInfo.rbxuid,
thumbnailType = thumbnailInfo.thumbnailType,
thumbnailSize = thumbnailInfo.thumbnailSize
}
end)
@@ -0,0 +1,39 @@
return function()
it("should return a table", function()
local action = require(script.Parent.FetchUserThumbnail)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.FetchUserThumbnail)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.FetchUserThumbnail)
expect(action.name).to.equal("FetchUserThumbnail")
end)
it("should set the rbxuid, thumbnailType and thumbnailSize", function()
local action = require(script.Parent.FetchUserThumbnail)(
{
rbxuid = 12345,
thumbnailType = Enum.ThumbnailType.HeadShot,
thumbnailSize = Enum.ThumbnailSize.Size180x180
})
expect(action.rbxuid).to.equal(12345)
expect(action.thumbnailType).to.equal(Enum.ThumbnailType.HeadShot)
expect(action.thumbnailSize).to.equal(Enum.ThumbnailSize.Size180x180)
end)
it("should set the type", function()
local action = require(script.Parent.FetchUserThumbnail)()
expect(action.type).to.equal("FetchUserThumbnail")
end)
end
@@ -0,0 +1,6 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("GetCrossPlayEnabledFailed", function()
return {}
end)
@@ -0,0 +1,25 @@
return function()
it("should return a table", function()
local action = require(script.Parent.GetCrossPlayEnabledFailed)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.GetCrossPlayEnabledFailed)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.GetCrossPlayEnabledFailed)
expect(action.name).to.equal("GetCrossPlayEnabledFailed")
end)
it("should set the type", function()
local action = require(script.Parent.GetCrossPlayEnabledFailed)()
expect(action.type).to.equal("GetCrossPlayEnabledFailed")
end)
end
@@ -0,0 +1,8 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("InsertScreen", function(item)
return {
item = item,
}
end)
@@ -0,0 +1,44 @@
return function()
describe("require", function()
it("should create without errors", function()
require(script.Parent.InsertScreen)
end)
it("should set the name", function()
local action = require(script.Parent.InsertScreen)
expect(action.name).to.equal("InsertScreen")
end)
end)
describe("call", function()
it("should return a table when called as a function", function()
local action = require(script.Parent.InsertScreen)
action = action({})
expect(action).to.be.a("table")
end)
it("should set the type", function()
local action = require(script.Parent.InsertScreen)
action = action({})
expect(action.type).to.equal("InsertScreen")
end)
it("should set the item", function()
local action = require(script.Parent.InsertScreen)
local item = "foo"
action = action(item)
expect(action.item).to.equal("foo")
end)
it("should set the type and name to be equal", function()
local action = require(script.Parent.InsertScreen)
local actionItem = action({})
expect(actionItem.type).to.equal(action.name)
end)
end)
end
@@ -0,0 +1,6 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("PostCrossPlayEnabledFailed", function()
return {}
end)
@@ -0,0 +1,25 @@
return function()
it("should return a table", function()
local action = require(script.Parent.PostCrossPlayEnabledFailed)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.PostCrossPlayEnabledFailed)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.PostCrossPlayEnabledFailed)
expect(action.name).to.equal("PostCrossPlayEnabledFailed")
end)
it("should set the type", function()
local action = require(script.Parent.PostCrossPlayEnabledFailed)()
expect(action.type).to.equal("PostCrossPlayEnabledFailed")
end)
end
@@ -0,0 +1,8 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("RemoveScreen", function(item)
return {
item = item,
}
end)
@@ -0,0 +1,44 @@
return function()
describe("require", function()
it("should create without errors", function()
require(script.Parent.RemoveScreen)
end)
it("should set the name", function()
local action = require(script.Parent.RemoveScreen)
expect(action.name).to.equal("RemoveScreen")
end)
end)
describe("call", function()
it("should return a table when called as a function", function()
local action = require(script.Parent.RemoveScreen)
action = action({})
expect(action).to.be.a("table")
end)
it("should set the type", function()
local action = require(script.Parent.RemoveScreen)
action = action({})
expect(action.type).to.equal("RemoveScreen")
end)
it("should set the item", function()
local action = require(script.Parent.RemoveScreen)
local item = "foo"
action = action(item)
expect(action.item).to.equal("foo")
end)
it("should set the type and name to be equal", function()
local action = require(script.Parent.RemoveScreen)
local actionItem = action({})
expect(actionItem.type).to.equal(action.name)
end)
end)
end
@@ -0,0 +1,6 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("RequestCrossPlayEnabled", function()
return {}
end)
@@ -0,0 +1,25 @@
return function()
it("should return a table", function()
local action = require(script.Parent.RequestCrossPlayEnabled)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.RequestCrossPlayEnabled)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.RequestCrossPlayEnabled)
expect(action.name).to.equal("RequestCrossPlayEnabled")
end)
it("should set the type", function()
local action = require(script.Parent.RequestCrossPlayEnabled)()
expect(action.type).to.equal("RequestCrossPlayEnabled")
end)
end
@@ -0,0 +1,6 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("ResetUserThumbnails", function()
return {}
end)
@@ -0,0 +1,25 @@
return function()
it("should return a table", function()
local action = require(script.Parent.ResetUserThumbnails)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.ResetUserThumbnails)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.ResetUserThumbnails)
expect(action.name).to.equal("ResetUserThumbnails")
end)
it("should set the type", function()
local action = require(script.Parent.ResetUserThumbnails)()
expect(action.type).to.equal("ResetUserThumbnails")
end)
end
@@ -0,0 +1,9 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("SetCrossPlayEnabled", function(enabled, timestamp)
return {
enabled = enabled,
timestamp = timestamp
}
end)
@@ -0,0 +1,32 @@
return function()
it("should return a table", function()
local action = require(script.Parent.SetCrossPlayEnabled)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.SetCrossPlayEnabled)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.SetCrossPlayEnabled)
expect(action.name).to.equal("SetCrossPlayEnabled")
end)
it("should set the enabled value and timestamp", function()
local action = require(script.Parent.SetCrossPlayEnabled)(true, 10)
expect(action.enabled).to.equal(true)
expect(action.timestamp).to.equal(10)
end)
it("should set the type", function()
local action = require(script.Parent.SetCrossPlayEnabled)()
expect(action.type).to.equal("SetCrossPlayEnabled")
end)
end
@@ -0,0 +1,22 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
--[[
// friendsData is table
// Table keys:
// [index number] - table
// xuid - number
// robloxName - string
// placeId - number
// robloxStatus - string
// robloxuid - number
// lastLocation - string
// gamertag - string
// xboxStatus - string
// friendsSource - string
]]
return Action("SetFriendsData", function(friendsData)
return {
data = friendsData
}
end)
@@ -0,0 +1,39 @@
return function()
it("should return a table", function()
local action = require(script.Parent.SetFriendsData)
expect(action).to.be.a("table")
end)
it("should return a table without data when passed nil", function()
local action = require(script.Parent.SetFriendsData)()
expect(action).to.be.a("table")
expect(action.data).to.equal(nil)
end)
it("should set the name", function()
local action = require(script.Parent.SetFriendsData)
expect(action.name).to.equal("SetFriendsData")
end)
it("should set the elements at the first depth", function()
local action = require(script.Parent.SetFriendsData)( { {}, {} } )
expect(action.data[2]).to.be.a("table")
end)
it("should set the elements at the second depth", function()
local action = require(script.Parent.SetFriendsData)( { { a="A", b="B" } } )
expect(action.data[1]).to.be.a("table")
expect(action.data[1].a).to.equal("A")
end)
it("should set the type", function()
local action = require(script.Parent.SetFriendsData)()
expect(action.type).to.equal("SetFriendsData")
end)
end
@@ -0,0 +1,12 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("SetPrivilegeSettings", function(privilegeSettings)
privilegeSettings = privilegeSettings or {}
return
{
Multiplayer = privilegeSettings.Multiplayer,
SharedContent = privilegeSettings.SharedContent,
timestamp = privilegeSettings.timestamp
}
end)
@@ -0,0 +1,33 @@
return function()
it("should return a table", function()
local action = require(script.Parent.SetPrivilegeSettings)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.SetPrivilegeSettings)()
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.SetPrivilegeSettings)
expect(action.name).to.equal("SetPrivilegeSettings")
end)
it("should set the privilege settings and timestamp", function()
local action = require(script.Parent.SetPrivilegeSettings)({Multiplayer = {}, SharedContent = {}, timestamp = 10})
expect(action.Multiplayer).to.be.a("table")
expect(action.SharedContent).to.be.a("table")
expect(action.timestamp).to.equal(10)
end)
it("should set the type", function()
local action = require(script.Parent.SetPrivilegeSettings)()
expect(action.type).to.equal("SetPrivilegeSettings")
end)
end
@@ -0,0 +1,22 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
--[[
// friendsData is table
// Table keys:
// [index number] - table
// xuid - number
// robloxName - string
// placeId - number
// robloxStatus - string
// robloxuid - number
// lastLocation - string
// gamertag - string
// xboxStatus - string
// friendsSource - string
]]
return Action("SetRenderedFriendsData", function(friendsData)
return {
data = friendsData
}
end)
@@ -0,0 +1,39 @@
return function()
it("should return a table", function()
local action = require(script.Parent.SetRenderedFriendsData)
expect(action).to.be.a("table")
end)
it("should return a table without data when passed nil", function()
local action = require(script.Parent.SetRenderedFriendsData)()
expect(action).to.be.a("table")
expect(action.data).to.equal(nil)
end)
it("should set the name", function()
local action = require(script.Parent.SetRenderedFriendsData)
expect(action.name).to.equal("SetRenderedFriendsData")
end)
it("should set the elements at the first depth", function()
local action = require(script.Parent.SetRenderedFriendsData)( { {}, {} } )
expect(action.data[2]).to.be.a("table")
end)
it("should set the elements at the second depth", function()
local action = require(script.Parent.SetRenderedFriendsData)( { { a="A", b="B" } } )
expect(action.data[1]).to.be.a("table")
expect(action.data[1].a).to.equal("A")
end)
it("should set the type", function()
local action = require(script.Parent.SetRenderedFriendsData)()
expect(action.type).to.equal("SetRenderedFriendsData")
end)
end
@@ -0,0 +1,11 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("SetRobloxUser", function(userInfo)
userInfo = userInfo or {}
return {
robloxName = userInfo.robloxName,
rbxuid = userInfo.rbxuid,
under13 = userInfo.under13
}
end)
@@ -0,0 +1,44 @@
return function()
it("should return a table", function()
local action = require(script.Parent.SetRobloxUser)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true})
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.SetRobloxUser)
expect(action.name).to.equal("SetRobloxUser")
end)
it("should set the robloxName, rbxuid, and under13 values", function()
local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true})
expect(action.robloxName).to.be.a("string")
expect(action.robloxName).to.equal("TestRobloxName")
expect(action.rbxuid).to.be.a("number")
expect(action.rbxuid).to.equal(12345)
expect(action.under13).to.be.a("boolean")
expect(action.under13).to.equal(true)
end)
it("should set the robloxName, rbxuid, and under13 values to nil when passed an empty table", function()
local action = require(script.Parent.SetRobloxUser)({})
expect(action.robloxName).to.equal(nil)
expect(action.rbxuid).to.equal(nil)
expect(action.under13).to.equal(nil)
end)
it("should set the type", function()
local action = require(script.Parent.SetRobloxUser)({robloxName="TestRobloxName", rbxuid=12345, under13 = true})
expect(action.type).to.equal("SetRobloxUser")
end)
end
@@ -0,0 +1,15 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("SetUserThumbnail", function(thumbnailInfo)
thumbnailInfo = thumbnailInfo or {}
return {
success = thumbnailInfo.success,
rbxuid = thumbnailInfo.rbxuid,
imageUrl = thumbnailInfo.imageUrl,
thumbnailType = thumbnailInfo.thumbnailType,
thumbnailSize = thumbnailInfo.thumbnailSize,
isFinal = thumbnailInfo.isFinal,
timestamp = thumbnailInfo.timestamp
}
end)
@@ -0,0 +1,10 @@
local CoreGui = game:GetService("CoreGui")
local Action = require(CoreGui.RobloxGui.Modules.Common.Action)
return Action("SetXboxUser", function(userInfo)
userInfo = userInfo or {}
return {
gamertag = userInfo.gamertag,
xuid = userInfo.xuid
}
end)
@@ -0,0 +1,41 @@
return function()
it("should return a table", function()
local action = require(script.Parent.SetXboxUser)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345})
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.SetXboxUser)
expect(action.name).to.equal("SetXboxUser")
end)
it("should set the gamertag and xuid values", function()
local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345})
expect(action.gamertag).to.be.a("string")
expect(action.gamertag).to.equal("TestGamerTag")
expect(action.xuid).to.be.a("number")
expect(action.xuid).to.equal(12345)
end)
it("should set gamertag and xuid to nil if passed an empty table", function()
local action = require(script.Parent.SetXboxUser)({})
expect(action.gamertag).to.equal(nil)
expect(action.xuid).to.equal(nil)
end)
it("should set the type", function()
local action = require(script.Parent.SetXboxUser)({gamertag="TestGamerTag", xuid=12345})
expect(action.type).to.equal("SetXboxUser")
end)
end
@@ -0,0 +1,65 @@
return function()
it("should return a table", function()
local action = require(script.Parent.SetUserThumbnail)
expect(action).to.be.a("table")
end)
it("should return a table when called as a function", function()
local action = require(script.Parent.SetUserThumbnail)(
{
success = true,
rbxuid = 12345,
imageUrl = "x",
thumbnailType = Enum.ThumbnailType.HeadShot,
thumbnailSize = Enum.ThumbnailSize.Size180x180,
isFinal = true,
timestamp = 10
})
expect(action).to.be.a("table")
end)
it("should set the name", function()
local action = require(script.Parent.SetUserThumbnail)
expect(action.name).to.equal("SetUserThumbnail")
end)
it("should set the success, rbxuid, imageUrl, thumbnailType, thumbnailSize and isFinal values", function()
local action = require(script.Parent.SetUserThumbnail)(
{
rbxuid = 12345,
thumbnailType = Enum.ThumbnailType.HeadShot,
thumbnailSize = Enum.ThumbnailSize.Size180x180,
success = true,
imageUrl = "x",
isFinal = true,
timestamp = 10
})
expect(action.success).to.equal(true)
expect(action.rbxuid).to.equal(12345)
expect(action.imageUrl).to.equal("x")
expect(action.thumbnailType).to.equal(Enum.ThumbnailType.HeadShot)
expect(action.thumbnailSize).to.equal(Enum.ThumbnailSize.Size180x180)
expect(action.isFinal).to.equal(true)
expect(action.timestamp).to.equal(10)
end)
it("should set the success, rbxuid, imageUrl, thumbnailType, thumbnailSize and isFinal to nil if passed an empty table", function()
local action = require(script.Parent.SetUserThumbnail)({})
expect(action.success).never.to.be.ok()
expect(action.rbxuid).never.to.be.ok()
expect(action.imageUrl).never.to.be.ok()
expect(action.thumbnailType).never.to.be.ok()
expect(action.thumbnailSize).never.to.be.ok()
expect(action.isFinal).never.to.be.ok()
end)
it("should set the type", function()
local action = require(script.Parent.SetUserThumbnail)({})
expect(action.type).to.equal("SetUserThumbnail")
end)
end
@@ -0,0 +1,59 @@
--[[
// Alerts.lua
// Global alert codes, each alert has an unique Id
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local Alerts =
{
Default = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("DefaultAlertPhrase"), Id = 0 };
UnlockedUGC = { Title = Strings:LocalizedString("UnlockedUGCTitle"), Msg = Strings:LocalizedString("UnlockedUGCPhrase"), Id = 101};
LostConnection =
{
Controller = { Title = Strings:LocalizedString("ControllerLostConnectionTitle"), Msg = Strings:LocalizedString("ControllerLostConnectionPhrase"), Id = 201 };
ActiveUser = { Title = Strings:LocalizedString("ActiveUserLostConnectionTitle"), Msg = Strings:LocalizedString("ActiveUserLostConnectionPhrase"), Id = 202 };
};
PlayMyPlaceMoreGames = { Title = Strings:LocalizedString('PlayMyPlaceMoreGamesTitle'), Msg = Strings:LocalizedString('PlayMyPlaceMoreGamesPhrase'), Id = 301 };
CrossPlatformPlayWelcome = { Title = Strings:LocalizedString("CPPWelcomeTitle"), Msg = Strings:LocalizedString("CPPWelcomePhrase"), Id = 401};
Reauthentication =
{
-- index mapped to int error code from c++, you must index into this with a string
[0] = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("ReauthUnknownPhrase"), Id = 1001 };
[1] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthSignedOutPhrase"), Id = 1002 };
[2] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1003 };
[3] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthInvalidSessionPhrase"), Id = 1004 };
[4] = { Title = Strings:LocalizedString("ReauthUnlinkTitle"), Msg = Strings:LocalizedString("ReauthUnlinkPhrase"), Id = 1005 };
[5] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1006 };
[6] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1007 };
[7] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1008 };
};
SignOut =
{
-- index mapped to int error code from c++, you must index into this with a string
[0] = { Title = Strings:LocalizedString("AlertOccurredTitle"), Msg = Strings:LocalizedString("ReauthUnknownPhrase"), Id = 1001 };
[1] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthSignedOutPhrase"), Id = 1002 };
[2] = { Title = Strings:LocalizedString("ReauthSignedOutTitle"), Msg = Strings:LocalizedString("ReauthInvalidSessionPhrase"), Id = 1003 };
[3] = { Title = Strings:LocalizedString("ReauthUnlinkTitle"), Msg = Strings:LocalizedString("ReauthUnlinkPhrase"), Id = 1004 };
[4] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1005 };
[5] = { Title = Strings:LocalizedString("ReauthRemovedTitle"), Msg = Strings:LocalizedString("ReauthRemovedPhrase"), Id = 1006 };
[6] = nil;
[7] = { Title = Strings:LocalizedString("ReauthMaintenanceTitle"), Msg = Strings:LocalizedString("ReauthMaintenancePhrase"), Id = 1007 };
};
PlatformLink = { Title = Strings:LocalizedString('PlatformLinkInfoTitle'), Msg = Strings:LocalizedString('PlatformLinkInfoMessage'), Id = 1201};
}
return Alerts
@@ -0,0 +1,173 @@
--[[
// Analytics.lua
// Fetches analytics data for console platform
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
--[[ Services ]]--
local AnalyticsService = nil
pcall(function() AnalyticsService = game:GetService('AnalyticsService') end)
local UserInputService = game:GetService('UserInputService')
local Analytics = {}
--[[ Helper Functions ]]--
local function setRBXEvent(eventName, additionalArgs)
local target, eventContext = nil, nil
local success, result = pcall(function()
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
target = "console"
eventContext = "XboxOne"
eventName = eventName or ""
additionalArgs = additionalArgs or {}
AnalyticsService:SetRBXEvent(target, eventContext, eventName, additionalArgs)
end
end)
if not success then
Utility.DebugLog("setRBXEvent() failed because", result, "Input: target:", target, " eventContext:", eventContext, " eventName:", eventName)
end
return success
end
local function setRBXEventStream(eventName, additionalArgs)
local target, eventContext = nil, nil
local success, result = pcall(function()
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
target = "console"
eventContext = "XboxOne"
eventName = eventName or ""
additionalArgs = additionalArgs or {}
AnalyticsService:SetRBXEventStream(target, eventContext, eventName, additionalArgs)
end
end)
if not success then
Utility.DebugLog("setRBXEventStream() failed because", result, "Input: target:", target, " eventContext:", eventContext, " eventName:", eventName)
end
return success
end
local function releaseRBXEventStream(eventName)
local target = nil
local success, result = pcall(function()
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
target = "console"
AnalyticsService:ReleaseRBXEventStream(target)
end
end)
if not success then
Utility.DebugLog("releaseRBXEventStream() failed because", result, "Input: target:", target)
end
return success
end
local function updateHeartbeatObject(additionalArgs)
local success, result = pcall(function()
AnalyticsService:UpdateHeartbeatObject(additionalArgs)
end)
if not success then
Utility.DebugLog("UpdateHeartbeatObject() failed because ", result, "Input: args:", additionalArgs)
end
return success
end
local function reportCounter(counterName, amount)
local success, result = pcall(function()
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
counterName = counterName or ""
counterName = "Xbox-"..tostring(counterName)
amount = amount or 1
AnalyticsService:ReportCounter(counterName, amount)
end
end)
if not success then
Utility.DebugLog("reportCounter() failed because", result, "Input: counterName:", counterName, "amount:", amount)
end
return success
end
--[[ Public API ]]--
function Analytics.SetRBXEvent(eventName, additionalArgs)
setRBXEvent(eventName, additionalArgs)
end
-- Non-real time
function Analytics.SetRBXEventStream(eventName, additionalArgs)
setRBXEventStream(eventName, additionalArgs)
end
function Analytics.UpdateHeartbeatObject(additionalArgs)
updateHeartbeatObject(additionalArgs)
end
-- Real time
function Analytics.ReportCounter(counterName, amount)
reportCounter(counterName, amount)
end
local WidgetNames = {
--Widget Name
["WidgetId"] = "WidgetName";
["AppHubId"] = "AppHub";
["AvatarEditorScreenId"] = "AvatarEditorScreen";
["AvatarPaneId"] = "AvatarPane";
["AvatarTileId"] = "AvatarTile";
["BadgeOverlayId"] = "BadgeOverlay";
["BaseCarouselScreenId"] = "BaseCarouselScreen";
["BaseOverlayId"] = "BaseOverlay";
["BaseTileId"] = "BaseTile";
["ConfirmPromptId"] = "ConfirmPrompt";
["DisableCrossplayOverlayId"] = "DisableCrossplayOverlay";
["EnableCrossplayOverlayId"] = "EnableCrossplayOverlay";
["EngagementScreenId"] = "EngagementScreen";
["ErrorOverlayId"] = "ErrorOverlay";
["GameDetailId"] = "GameDetail";
["GameGenreScreenId"] = "GameGenreScreen";
["GamesPaneId"] = "GamesPane";
["GameSearchScreenId"] = "GameSearchScreen";
["HomePaneId"] = "HomePane";
["ImageOverlayId"] = "ImageOverlay";
["LinkAccountScreenId"] = "LinkAccountScreen"; --AKA "Sign In Screen"
["NoActionOverlayId"] = "NoActionOverlay";
["OutfitTileId"] = "OutfitTile";
["OverscanScreenId"] = "OverscanScreen";
["PurchasePackagePromptId"] = "PurchasePackagePrompt";
["ReportOverlayId"] = "ReportOverlay";
["RobuxBalanceOverlayId"] = "RobuxBalanceOverlay";
["SetAccountCredentialsScreenId"] = "SetAccountCredentialsScreen"; --AKA "Sign Up Screen" and "Microwave Screen" (User sees it as two different screens)
["SettingsScreenId"] = "SettingsScreen";
["SideBarId"] = "SideBar";
["SignInScreenId"] = "SignInScreen"; --AKA "Choice Screen"
["SocialPaneId"] = "SocialPane";
["StorePaneId"] = "StorePane";
["TabDockId"] = "TabDock";
["UnlinkAccountOverlayId"] = "UnlinkAccountOverlay";
["UnlinkAccountScreenId"] = "UnlinkAccountScreen";
}
function Analytics.WidgetNames(stringKey)
local result = WidgetNames and WidgetNames[stringKey]
if not result then
Utility.DebugLog("Analytics.WidgetNames: Could not find widget name for:" , stringKey)
result = stringKey
end
return result
end
return Analytics
@@ -0,0 +1,66 @@
-- Creates UI for Action/Title safe container as per Microsofts reccomendations
local CoreGui = game:GetService('CoreGui')
local RobloxGui = CoreGui.RobloxGui
local ShellModules = script.Parent
local Utility = require(ShellModules.Utility)
local ACTION_SAFE_INSET = UDim2.new((128 / 1920) * 0.5, 0, (64 / 1080) * 0.5, 0)
local TITLE_SAFE_INSET = UDim2.new((72 / 1792) * 0.5, 0, (16 / 1080) * 0.5, 0)
local AppContainer = {}
AppContainer.Root = Utility.Create'Frame'
{
Name = "AppContainerRoot";
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
BorderSizePixel = 0;
BackgroundColor3 = Color3.new(0, 0, 0);
ClipsDescendants = true;
Parent = RobloxGui;
}
AppContainer.AspectRatioProtector = Utility.Create'Frame'
{
Name = 'AspectRatioProtector';
Size = UDim2.new(1, 0, 1, 0);
Position = UDim2.new(0,0,0,0);
BackgroundTransparency = 1;
Parent = AppContainer.Root;
}
AppContainer.ActionSafeContainer = Utility.Create'Frame'
{
Name = "ActionSafeContainer";
Size = UDim2.new(1, 0, 1, 0) - (ACTION_SAFE_INSET + ACTION_SAFE_INSET);
Position = UDim2.new(0,0,0,0) + ACTION_SAFE_INSET;
BackgroundTransparency = 1;
Parent = AppContainer.AspectRatioProtector;
}
AppContainer.TitleSafeContainer = Utility.Create'Frame'
{
Name = "TitleSafeContainer";
Size = UDim2.new(1, 0, 1, 0) - (TITLE_SAFE_INSET + TITLE_SAFE_INSET);
Position = UDim2.new(0, 0, 0, 0) + TITLE_SAFE_INSET;
BackgroundTransparency = 1;
Parent = AppContainer.ActionSafeContainer;
}
local function OnAbsoluteSizeChanged()
local newSize = Utility.CalculateFit(AppContainer.Root, Vector2.new(16,9))
if newSize ~= AppContainer.AspectRatioProtector.Size then
AppContainer.AspectRatioProtector.Size = newSize
AppContainer.AspectRatioProtector.AnchorPoint = Vector2.new(0.5, 0.5)
AppContainer.AspectRatioProtector.Position = UDim2.new(0.5, 0, 0.5, 0)
end
end
AppContainer.Root:GetPropertyChangedSignal('AbsoluteSize'):connect(function()
OnAbsoluteSizeChanged()
end)
OnAbsoluteSizeChanged()
return AppContainer
@@ -0,0 +1,198 @@
-- Written by Kip Turner, Copyright Roblox 2015
-- App's Main
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:FindFirstChild("RobloxGui")
local Modules = RobloxGui:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
-- TODO: Will use for re-auth when finished
local UserInputService = game:GetService('UserInputService')
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local ThirdPartyUserService = nil
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
local Players = game:GetService("Players")
-- Start up background scene before anything else
require(ShellModules.BackgroundSceneManager)
local Utility = require(ShellModules:FindFirstChild('Utility'))
local AppHubModule = require(ShellModules:FindFirstChild('AppHub'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local Errors = require(ShellModules:FindFirstChild('Errors'))
local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay'))
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
local GameGenreScreen = require(ShellModules:FindFirstChild('GameGenreScreen'))
local EngagementScreenModule = require(ShellModules:FindFirstChild('EngagementScreen'))
local BadgeScreenModule = require(ShellModules:FindFirstChild('BadgeScreen'))
local AccountScreen = require(ShellModules:FindFirstChild('AccountScreen'))
local UserData = require(ShellModules:FindFirstChild('UserData'))
local ControllerStateManager = require(ShellModules:FindFirstChild('ControllerStateManager'))
local Alerts = require(ShellModules:FindFirstChild('Alerts'))
local SiteInfoWidget = require(ShellModules:FindFirstChild('SiteInfoWidget'))
local GameDetailModule = require(ShellModules:FindFirstChild('GameDetailScreen'))
-- Initialize AppState and in turn initialize the Store
local AppState = require(ShellModules.AppState)
local AppContainer = require(ShellModules.AppContainer)
local TitleSafeContainer = AppContainer.TitleSafeContainer
local EngagementScreen = EngagementScreenModule()
EngagementScreen:SetParent(TitleSafeContainer)
-- Site Info View
SiteInfoWidget.new()
-- Initialzie Account Age View
require(ShellModules.Components.AccountAgeStatus).new(AppState.store, TitleSafeContainer)
local function returnToEngagementScreen()
if ScreenManager:ContainsScreen(EngagementScreen) then
while ScreenManager:GetTopScreen() ~= EngagementScreen do
ScreenManager:CloseCurrent()
end
else
while ScreenManager:GetTopScreen() do
ScreenManager:CloseCurrent()
end
ScreenManager:OpenScreen(EngagementScreen)
end
end
local AppHub = nil
local function onAuthenticationSuccess(isNewLinkedAccount)
-- Set UserData
UserData:Initialize()
local SetRobloxUser = require(ShellModules.Actions.SetRobloxUser)
AppState.store:dispatch(SetRobloxUser( {
robloxName = Players.LocalPlayer.Name,
rbxuid = Players.LocalPlayer.UserId,
under13 = Players.LocalPlayer:GetUnder13(),
} ))
-- Unwind Screens if needed - this will be needed once we put in account linking
returnToEngagementScreen()
AppHub = AppHubModule()
AppHub:SetParent(TitleSafeContainer)
EventHub:addEventListener(EventHub.Notifications["OpenGameDetail"], "gameDetail",
function(placeId)
local gameDetail = GameDetailModule(placeId)
gameDetail:SetParent(TitleSafeContainer);
ScreenManager:OpenScreen(gameDetail);
end);
EventHub:addEventListener(EventHub.Notifications["OpenGameGenre"], "gameGenre",
function(sortName, gameCollection)
local gameGenre = GameGenreScreen(sortName, gameCollection)
gameGenre:SetParent(TitleSafeContainer);
ScreenManager:OpenScreen(gameGenre);
end);
EventHub:addEventListener(EventHub.Notifications["OpenBadgeScreen"], "gameBadges",
function(badgeData, previousScreenName)
local badgeScreen = BadgeScreenModule(badgeData, previousScreenName)
badgeScreen:SetParent(TitleSafeContainer);
ScreenManager:OpenScreen(badgeScreen);
end)
EventHub:addEventListener(EventHub.Notifications["OpenSettingsScreen"], "settingsScreen",
function(settingsScreen)
settingsScreen:SetParent(TitleSafeContainer);
ScreenManager:OpenScreen(settingsScreen);
end)
EventHub:addEventListener(EventHub.Notifications["OpenAvatarEditorScreen"], "avatarEditorScreen",
function(screen)
screen:SetParent(AppContainer.Root);
ScreenManager:OpenScreen(screen);
end)
EventHub:addEventListener(EventHub.Notifications["OpenAccountSettingsScreen"], "accountSettingsScreen",
function(errorCode)
local accountScreen = AccountScreen(errorCode)
accountScreen:SetParent(TitleSafeContainer);
ScreenManager:OpenScreen(accountScreen);
end)
Utility.DebugLog("User and Event initialization finished. Opening AppHub")
ScreenManager:OpenScreen(AppHub);
if PlatformService and not PlatformService:SeenCPPWelcomeMsg() then
ScreenManager:OpenScreen(ErrorOverlay(Alerts.CrossPlatformPlayWelcome), false)
end
-- Comment out for now since this has never been called in current flow
-- show info popup to users on newly linked accounts
-- if isNewLinkedAccount == true then
-- ScreenManager:OpenScreen(ErrorOverlay(Alerts.PlatformLink), false)
-- end
end
local function onReAuthentication(reauthenticationReason)
Utility.DebugLog("Beging Reauth, cleaning things up")
local SetRobloxUser = require(ShellModules.Actions.SetRobloxUser)
AppState.store:dispatch(SetRobloxUser())
local SetXboxUser = require(ShellModules.Actions.SetXboxUser)
AppState.store:dispatch(SetXboxUser())
-- unwind ScreenManager
returnToEngagementScreen()
UserData:Reset()
AppHub = nil
EventHub:removeEventListener(EventHub.Notifications["OpenGameDetail"], "gameDetail")
EventHub:removeEventListener(EventHub.Notifications["OpenGameGenre"], "gameGenre")
EventHub:removeEventListener(EventHub.Notifications["OpenBadgeScreen"], "gameBadges")
EventHub:removeEventListener(EventHub.Notifications["OpenSettingsScreen"], "settingsScreen")
EventHub:removeEventListener(EventHub.Notifications["OpenAvatarEditorScreen"], "avatarEditorScreen")
EventHub:removeEventListener(EventHub.Notifications["OpenAccountSettingsScreen"], "accountSettingsScreen")
Utility.DebugLog("Reauth complete. Return to engagement screen.")
-- show reason overlay
local alert = Alerts.SignOut[reauthenticationReason] or Alerts.Default
if reauthenticationReason == 6 then
alert = nil
end
if alert then
ScreenManager:OpenScreen(ErrorOverlay(alert), false)
end
end
local function onGameJoin(joinResult, placeId)
-- 0 is success, anything else is an error
local joinSuccess = joinResult == 0
if not joinSuccess then
local err = Errors.GameJoin[joinResult] or Errors.GameJoin.Default
ScreenManager:OpenScreen(ErrorOverlay(err), false)
end
EventHub:dispatchEvent(EventHub.Notifications["GameJoin"], joinSuccess, placeId)
end
if PlatformService then
PlatformService.GameJoined:connect(onGameJoin)
end
if ThirdPartyUserService then
ThirdPartyUserService.ActiveUserSignedOut:connect(onReAuthentication)
end
ControllerStateManager:Initialize()
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "authUserSuccess", function(isNewLinkedAccount)
Utility.DebugLog("User authenticated, initializing app shell")
onAuthenticationSuccess(isNewLinkedAccount)
end);
ScreenManager:OpenScreen(EngagementScreen)
UserInputService.MouseIconEnabled = false
return {}
@@ -0,0 +1,437 @@
-- Written by Kip Turner, Copyright Roblox 2015
local CoreGui = game:GetService("CoreGui")
local ContextActionService = game:GetService("ContextActionService")
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService('GuiService')
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local AppTabDockModule = require(ShellModules:FindFirstChild('TabDock'))
local AppTabDockItemModule = require(ShellModules:FindFirstChild('TabDockItem'))
local HomePaneModule = require(ShellModules:FindFirstChild('HomePane'))
local GamesPaneModule = require(ShellModules:FindFirstChild('GamesPane'))
local AvatarPaneModule = require(ShellModules:FindFirstChild('AvatarPane'))
local Errors = require(ShellModules:FindFirstChild('Errors'))
local ErrorOverlay = require(ShellModules:FindFirstChild('ErrorOverlay'))
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local SocialPaneModule = require(ShellModules.Components.Social.SocialPane)
local StorePaneModule = require(ShellModules:FindFirstChild('StorePane'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local SettingsScreen = require(ShellModules:FindFirstChild('SettingsScreen'))
local AvatarEditorScreen = require(ShellModules:FindFirstChild('AvatarEditorScreen'))
local GameSearchScreen = require(ShellModules:FindFirstChild('GameSearchScreen'))
local AchievementManager = require(ShellModules:FindFirstChild('AchievementManager'))
local HintActionView = require(ShellModules:FindFirstChild('HintActionView'))
local function CreateAppHub()
local this = {}
local AppTabDock = AppTabDockModule(
UDim2.new(0,0,0.132,0),
UDim2.new(0,0,0,0))
local appHubCns = {}
local isShown = false
local lastSelectedContentPane = nil
local lastParent = nil
local HubContainer = Utility.Create'Frame'
{
Name = 'HubContainer';
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
Visible = false;
}
local PaneContainer = Utility.Create'Frame'
{
Name = 'PaneContainer';
Size = UDim2.new(1, 0, 0.786, 0);
Position = UDim2.new(0,0,0.214,0);
BackgroundTransparency = 1;
Parent = HubContainer;
}
AppTabDock:SetParent(HubContainer)
local HomeTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('HomeWord'),
HomePaneModule(PaneContainer)))
local AvatarTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('AvatarWord'),
AvatarPaneModule(PaneContainer)))
local GameTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('GamesWord'),
GamesPaneModule(PaneContainer)))
local SocialTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('FriendsWord'),
SocialPaneModule(PaneContainer)))
local StoreTab = AppTabDock:AddTab(AppTabDockItemModule(Strings:LocalizedString('CatalogWord'),
StorePaneModule(PaneContainer)))
Utility.Create'ImageLabel'
{
Name = 'RobloxLogo';
Size = UDim2.new(0, 232, 0, 56);
Position = UDim2.new(0,0,0,0);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Icons/ROBLOXLogoSmall@1080.png';
Parent = HubContainer;
}
local function SetSelectedTab(newTab)
AppTabDock:SetSelectedTab(newTab)
end
-- Hint Action View
local hintActionViewX = HintActionView(HubContainer, "OpenHintAction")
hintActionViewX:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/XButton.png') -- always X button for tab views
hintActionViewX:SetVisible(false)
local hintActionViewY = HintActionView(HubContainer, "OpenYHintAction", UDim2.new(0, 0, 1, -1))
hintActionViewY:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/YButton.png')
hintActionViewY:SetVisible(false)
-- Action Functions
local seenXButtonPressed = false
local seenYButtonPressed = false
local function onOpenSettings(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenXButtonPressed = true
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
local settingsScreen = SettingsScreen()
EventHub:dispatchEvent(EventHub.Notifications["OpenSettingsScreen"], settingsScreen);
end
end
local function onOpenAvatarEditor(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenYButtonPressed = true
elseif inputState == Enum.UserInputState.End and seenYButtonPressed then
local avatarEditorScreen = AvatarEditorScreen()
EventHub:dispatchEvent(EventHub.Notifications["OpenAvatarEditorScreen"], avatarEditorScreen);
seenYButtonPressed = false
end
end
local function onOpenPartyUI(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenXButtonPressed = true
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
if UserSettings().GameSettings:InStudioMode() or UserInputService:GetPlatform() == Enum.Platform.Windows then
ScreenManager:OpenScreen(ErrorOverlay(Errors.Test.FeatureNotAvailableInStudio), false)
else
local success = pcall(function()
-- PlatformService may not exist in studio
return PlatformService:PopupPartyUI(inputObject.UserInputType)
end)
if not success then
ScreenManager:OpenScreen(ErrorOverlay(Errors.PlatformError.PopupPartyUI), false)
end
end
end
end
local function onSearchGames(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenXButtonPressed = true
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
if PlatformService then
PlatformService:ShowKeyboard(Strings:LocalizedString("SearchGamesPhrase"), "", "", Enum.XboxKeyBoardType.Default)
end
seenXButtonPressed = false
end
end
local function bindHintActionX(actionFunc, actionName)
hintActionViewX:SetText(actionName)
hintActionViewX:BindAction(actionFunc, Enum.KeyCode.ButtonX)
hintActionViewX:SetVisible(true)
end
local function setHintAction(selectedTab)
hintActionViewX:UnbindAction()
hintActionViewX:SetVisible(false)
hintActionViewY:UnbindAction()
hintActionViewY:SetVisible(false)
if selectedTab == HomeTab then
bindHintActionX(onOpenSettings, Strings:LocalizedString("SettingsWord"))
elseif selectedTab == GameTab then
if AchievementManager:AllGamesUnlocked() then
bindHintActionX(onSearchGames, Strings:LocalizedString("SearchWord"))
end
elseif selectedTab == SocialTab then
bindHintActionX(onOpenPartyUI, Strings:LocalizedString("StartPartyPhrase"))
elseif selectedTab == AvatarTab then
hintActionViewY:SetText(Strings:LocalizedString("AvatarEditorWord"))
hintActionViewY:BindAction(onOpenAvatarEditor, Enum.KeyCode.ButtonY)
hintActionViewY:SetVisible(true)
end
-- NOTE: Avatar Tab has its own HintActionView as it needs to change visibility based on whats selected
end
function this:GetName()
return lastSelectedContentPane and lastSelectedContentPane:GetName() or Strings:LocalizedString('HomeWord')
end
--For analytics
function this:GetAnalyticsInfo()
local analyticsInfo = {}
local WidgetId = Analytics.WidgetNames('WidgetId')
local paneAnalyticsInfo = nil
if lastSelectedContentPane and type(lastSelectedContentPane.GetAnalyticsInfo) == "function" then
paneAnalyticsInfo = lastSelectedContentPane.GetAnalyticsInfo()
end
--paneAnalyticsInfo should never be nil
if type(paneAnalyticsInfo) == "table" and paneAnalyticsInfo[WidgetId] then
analyticsInfo = paneAnalyticsInfo
else
analyticsInfo[WidgetId] = Analytics.WidgetNames('AppHubId')
end
return analyticsInfo
end
function this:Show()
isShown = true
HubContainer.Visible = true
HubContainer.Parent = lastParent
EventHub:removeEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch')
EventHub:addEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch',
function()
if ScreenManager:ContainsScreen(this) then
while ScreenManager:GetTopScreen() ~= this and ScreenManager:ContainsScreen(this) do
ScreenManager:CloseCurrent()
end
if ScreenManager:GetTopScreen() == this then
if AppTabDock:GetSelectedTab() ~= StoreTab then
SetSelectedTab(StoreTab)
end
end
end
end)
local openEquippedDebounce = false
EventHub:removeEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch')
EventHub:addEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch',
function()
if openEquippedDebounce then return end
openEquippedDebounce = true
if ScreenManager:ContainsScreen(this) then
while ScreenManager:GetTopScreen() ~= this and ScreenManager:ContainsScreen(this) do
ScreenManager:CloseCurrent()
end
if ScreenManager:GetTopScreen() == this then
if AppTabDock:GetSelectedTab() ~= AvatarTab then
SetSelectedTab(AvatarTab)
end
end
end
openEquippedDebounce = false
end)
if not AchievementManager:AllGamesUnlocked() then
EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], 'AppHubUnlockedUGC')
EventHub:addEventListener(EventHub.Notifications["UnlockedUGC"], "AppHubUnlockedUGC", function()
if isShown then
local selectedTab = AppTabDock:GetSelectedTab()
if selectedTab and selectedTab == GameTab then
hintActionViewX:UnbindAction()
hintActionViewX:SetVisible(false)
bindHintActionX(onSearchGames, Strings:LocalizedString("SearchWord"))
end
end
end)
end
local currentlySelectedTab = AppTabDock:GetSelectedTab()
AppTabDock:SetSelectedTab(currentlySelectedTab)
if lastSelectedContentPane then
lastSelectedContentPane:Show()
end
end
function this:Hide()
isShown = false
if not ScreenManager:ContainsScreen(self) then
EventHub:removeEventListener(EventHub.Notifications["NavigateToRobuxScreen"], 'AppHubListenToRobuxScreenSwitch')
EventHub:removeEventListener(EventHub.Notifications["NavigateToEquippedAvatar"], 'AppHubListenToAvatarScreenSwitch')
end
EventHub:removeEventListener(EventHub.Notifications["UnlockedUGC"], 'AppHubUnlockedUGC')
HubContainer.Visible = false
HubContainer.Parent = nil
if lastSelectedContentPane then
lastSelectedContentPane:Hide()
end
end
function this:Focus()
AppTabDock:ConnectEvents()
local function initTabDock()
ContextActionService:BindCoreAction("CycleTabDock",
function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
if not AppTabDock:IsFocused() then
lastSelectedContentPane:RemoveFocus(true)
AppTabDock:Focus()
else
if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then
local prevTab = AppTabDock:GetPreviousTab()
if prevTab then
AppTabDock:SetSelectedTab(prevTab)
end
elseif inputObject.KeyCode == Enum.KeyCode.ButtonR1 then
local nextTab = AppTabDock:GetNextTab()
if nextTab then
AppTabDock:SetSelectedTab(nextTab)
end
end
end
end
end,
false,
Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1)
local seenBButtonBegin = false
ContextActionService:BindCoreAction("CloseAppHub",
function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenBButtonBegin = true
elseif inputState == Enum.UserInputState.End then
if seenBButtonBegin then
if not AppTabDock:IsFocused() then
lastSelectedContentPane:RemoveFocus(true)
AppTabDock:Focus()
end
end
end
end,
false,
Enum.KeyCode.ButtonB)
local function focusTab(tab)
if tab then
if lastSelectedContentPane then
lastSelectedContentPane:Hide(true)
lastSelectedContentPane:RemoveFocus(true)
end
local selectedContentPane = tab:GetContentItem()
if selectedContentPane then
selectedContentPane:Show(true)
if not AppTabDock:IsFocused() then
AppTabDock:Focus()
end
end
lastSelectedContentPane = selectedContentPane
-- set X actionf
setHintAction(tab)
end
end
local function onSelectedTabChanged(selectedTab)
focusTab(selectedTab)
end
table.insert(appHubCns, AppTabDock.SelectedTabChanged:connect(onSelectedTabChanged))
end
initTabDock()
local function onSelectedTabClicked(selectedTab)
local selectedContentPane = selectedTab and selectedTab:GetContentItem()
if selectedContentPane then
selectedContentPane:Focus(AppTabDock)
end
end
table.insert(appHubCns, AppTabDock.SelectedTabClicked:connect(onSelectedTabClicked))
local function onSelectionChanged(prop)
if prop == "SelectedCoreObject" then
if AppTabDock:IsFocused() then
AppTabDock:Show()
if lastSelectedContentPane then
lastSelectedContentPane:RemoveFocus()
end
end
end
if prop == "SelectedObject" then
local currentSelection = GuiService.SelectedCoreObject
if currentSelection and lastSelectedContentPane then
-- first condition checks if function exist
if lastSelectedContentPane.IsFocused and not lastSelectedContentPane:IsFocused() and
lastSelectedContentPane.IsAncestorOf then
if lastSelectedContentPane:IsAncestorOf(currentSelection) then
lastSelectedContentPane:Focus(AppTabDock)
end
end
end
end
end
table.insert(appHubCns, GuiService.Changed:connect(onSelectionChanged))
local function onKeyboardClosed(searchWord)
searchWord = Utility.SpaceNormalizeString(searchWord)
if #searchWord > 0 then
local searchScreen = GameSearchScreen(searchWord)
searchScreen:SetParent(HubContainer.Parent)
ScreenManager:OpenScreen(searchScreen)
end
end
if PlatformService then
table.insert(appHubCns, PlatformService.KeyboardClosed:connect(onKeyboardClosed))
end
if AppTabDock:GetSelectedTab() == nil then
AppTabDock:SetSelectedTab(HomeTab)
end
if lastSelectedContentPane then
lastSelectedContentPane:Focus(AppTabDock)
end
setHintAction(AppTabDock:GetSelectedTab())
end
function this:RemoveFocus()
AppTabDock:DisconnectEvents()
ContextActionService:UnbindCoreAction("CycleTabDock")
ContextActionService:UnbindCoreAction("CloseAppHub")
if lastSelectedContentPane then
lastSelectedContentPane:RemoveFocus()
end
for k,v in pairs(appHubCns) do
v:disconnect()
appHubCns[k] = nil
end
ContextActionService:UnbindCoreAction("OpenHintAction")
ContextActionService:UnbindCoreAction("OpenYHintAction")
end
function this:SetParent(newParent)
lastParent = newParent
end
return this
end
return CreateAppHub
@@ -0,0 +1,23 @@
-- for now AppState will be singleton so we can more easily migrate to Rodux
-- but if we also migrate to Roact, this will need to change
local ShellModules = script.Parent
local Modules = ShellModules.Parent
local Common = Modules.Common
local AppShellReducer = require(ShellModules.Reducers.AppShellReducer)
local Store = require(Common.Rodux).Store
local AppState = {}
function AppState:Init()
self.store = Store.new(AppShellReducer)
end
function AppState:Destruct()
self.store:destruct()
end
AppState:Init()
return AppState
@@ -0,0 +1,27 @@
-- Written by Kip Turner, Copyright Roblox 2015
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui:FindFirstChild("RobloxGui")
local Modules = RobloxGui:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
local AssetManager = {}
function AssetManager.CreateShadow(zIndex)
return Utility.Create'ImageLabel'
{
Name = 'Shadow';
Image = 'rbxasset://textures/ui/Shell/Buttons/Generic9ScaleShadow.png';
Size = UDim2.new(1,3,1,3);
Position = UDim2.new(0,0,0,0);
ScaleType = Enum.ScaleType.Slice;
SliceCenter = Rect.new(10,10,28,28);
BackgroundTransparency = 1;
ZIndex = zIndex or 1;
}
end
return AssetManager
@@ -0,0 +1,129 @@
local CoreGui = game:GetService('CoreGui')
local GuiRoot = CoreGui:FindFirstChild('RobloxGui')
local Modules = GuiRoot:FindFirstChild('Modules')
local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState)
local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities)
local TweenController = require(Modules.LuaApp.Legacy.AvatarEditor.TweenInstanceController)
local ShellModules = Modules:FindFirstChild('Shell')
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local CameraManager = require(ShellModules:FindFirstChild('CameraManager'))
local CreateAvatarEditorView = require(ShellModules:FindFirstChild('AvatarEditorView'))
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
local AppContainer = require(ShellModules.AppContainer)
local TitleSafeContainer = AppContainer.TitleSafeContainer
local ThirdPartyUserService = nil
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
local AvatarEditorView = nil
local AvatarEditorReconstruct = false
local function createAvatarEditorScreen()
local this = BaseScreen()
this.fixPosition = true
local view = this.GetView()
view.BackImage.ZIndex = 2
view.BackText.ZIndex = 2
view.TitleText.ZIndex = 2
local adjustPos = UDim2.new(0, TitleSafeContainer.AbsolutePosition.X, 0, TitleSafeContainer.AbsolutePosition.Y)
local backImagePosition = view.BackImage.Position + adjustPos
local backTextPosition = view.BackText.Position + adjustPos
local titleTextPosition = view.TitleText.Position + adjustPos
view.BackImage.Position = backImagePosition
view.BackText.Position = backTextPosition
view.TitleText.Position = titleTextPosition
--Reconstruct the AvatarEditorView if user switched/assets purchased
if AvatarEditorReconstruct and AvatarEditorView then
AvatarEditorView:Destruct()
AvatarEditorView = nil
AvatarEditorReconstruct = false
end
AvatarEditorView = AvatarEditorView or CreateAvatarEditorView()
local storeChangedCn = nil
local tweenInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local tweenPositionOffset = UDim2.new(0, -400, 0, 0)
local parentShow = this.Show
function this:Show()
parentShow(self)
CameraManager:SwitchToAvatarEditor()
AvatarEditorView:Show(this.Container)
end
local parentHide = this.Hide
function this:Hide()
parentHide(self)
AvatarEditorView:Hide()
CameraManager:SwitchToFlyThrough()
end
local parentFocus = this.Focus
function this:Focus()
parentFocus(self)
storeChangedCn = AppState.Store.changed:connect(function(newState, oldState)
if newState.FullView ~= oldState.FullView then
if newState.FullView then
TweenController(view.BackImage, tweenInfo, { Position = backImagePosition + tweenPositionOffset })
TweenController(view.BackText, tweenInfo, { Position = backTextPosition + tweenPositionOffset })
TweenController(view.TitleText, tweenInfo, { Position = titleTextPosition + tweenPositionOffset })
else
TweenController(view.BackImage, tweenInfo, { Position = backImagePosition })
TweenController(view.BackText, tweenInfo, { Position = backTextPosition })
TweenController(view.TitleText, tweenInfo, { Position = titleTextPosition })
end
end
end)
AvatarEditorView:Focus()
end
local parentRemoveFocus = this.RemoveFocus
function this:RemoveFocus()
parentRemoveFocus(self)
AvatarEditorView:RemoveFocus()
Utilities.disconnectEvent(storeChangedCn)
--Don't save selectedObject, as we already saved it in AvatarEditorView
self.SavedSelectedObject = nil
end
local parentSetParent = this.SetParent
function this:SetParent(newParent)
parentSetParent(self, newParent)
end
function this:GetAnalyticsInfo()
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarEditorScreenId')}
end
return this
end
local function OnUserAccountChanged()
AvatarEditorReconstruct = true
EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen")
EventHub:addEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen",
function()
AvatarEditorReconstruct = true
end)
end
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "AvatarEditorScreen", OnUserAccountChanged)
local function OnUserSignOut()
AvatarEditorReconstruct = true
EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "AvatarEditorScreen")
end
if ThirdPartyUserService then
ThirdPartyUserService.ActiveUserSignedOut:connect(OnUserSignOut)
end
return createAvatarEditorScreen
@@ -0,0 +1,516 @@
-------------- CONSTANTS --------------
local STICK_ROTATION_MULTIPLIER = 3
local THUMBSTICK_DEADZONE = 0.2
-------------- SERVICES --------------
local ContextActionService = game:GetService("ContextActionService")
local ReplicatedStorage = game:GetService('ReplicatedStorage')
local CoreGui = game:GetService('CoreGui')
------------ MODULES -------------------
local GuiRoot = CoreGui:FindFirstChild('RobloxGui')
local Modules = GuiRoot:FindFirstChild('Modules')
local AppState = require(Modules.LuaApp.Legacy.AvatarEditor.AppState)
local SetConsoleMenuLevel = require(Modules.LuaApp.Actions.SetConsoleMenuLevel)
local ResetCategory = require(Modules.LuaApp.Actions.ResetCategory)
local ToggleAvatarType = require(Modules.LuaApp.Actions.ToggleAvatarType)
local ToggleAvatarEditorFullView = require(Modules.LuaApp.Actions.ToggleAvatarEditorFullView)
local CreateCharacterManager = require(Modules.LuaApp.Legacy.AvatarEditor.CharacterManager)
local CreatePageManager = require(Modules.LuaApp.Legacy.AvatarEditor.PageManagerConsole)
local CreateCategoryMenu = require(Modules.LuaApp.Legacy.AvatarEditor.CategoryMenuConsole)
local CreateTabList = require(Modules.LuaApp.Legacy.AvatarEditor.TabListConsole)
local ConsoleButtonIndicators = require(Modules.LuaApp.Legacy.AvatarEditor.ConsoleButtonIndicators)
local LayoutInfo = require(Modules.LuaApp.Legacy.AvatarEditor.LayoutInfoConsole)
local Flags = require(Modules.LuaApp.Legacy.AvatarEditor.Flags)
local XboxAppState = require(Modules.Shell.AppState)
----------- UTILITIES --------------
local Utilities = require(Modules.LuaApp.Legacy.AvatarEditor.Utilities)
local TableUtilities = require(Modules.LuaApp.TableUtilities)
------------ SHELL MODULES -------------------
local ShellModules = script.Parent
local CameraManager = require(ShellModules:FindFirstChild('CameraManager'))
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget'))
-------------- FFLAGS --------------
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
------------ VARIABLES -------------------
local characterTemplates = {
CharacterR6 = ReplicatedStorage:WaitForChild('CharacterR6');
CharacterR15 = ReplicatedStorage:WaitForChild('CharacterR15');
CharacterR15New = ReplicatedStorage:WaitForChild('CharacterR15New');
}
local cameraTweenerObject = {
tweenCamera = function(newCFrame, newFOV)
CameraManager:UpdateAvatarEditorCamera(newCFrame, newFOV)
end
}
----------- CLASS DECLARATION --------------
local function createAvatarEditorView()
AppState:Init()
local this = {}
local storeChangedCn = nil
local scrollingFrameCn = nil
local avatarEditorLoader = nil
local savedSelectedGuiObject = nil
local selectedGuiObjectEnterFullView = nil
local toggleButtonDebounce = true
local toggleViewDebounce = true
local showCount = 0
local inShow = false
local inFocus = false
local lastCharacterSaveTime = nil
local characterUpdated = false
local characterEquipped = false
local savingAvatarMutex = false
local rotationInfo = {
rotation = 0;
lastRotation = 0;
delta = 0;
}
local Container = Utilities.create'ScrollingFrame'
{
Name = 'AvatarEditorContainer';
Size = UDim2.new(1, 0, 1, 0);
CanvasSize = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
ScrollingEnabled = false;
Selectable = false;
Visible = true;
BorderSizePixel = 0;
ScrollBarThickness = 0;
}
local BackgroundOverlay = Utilities.create'ImageLabel'
{
Name = 'BackgroundOverlay';
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
Visible = true;
Image = 'rbxasset://textures/ui/Shell/AvatarEditor/graphic/gr-background overlay merge.png';
ZIndex = LayoutInfo.BackgroundLayer;
Parent = Container;
}
local frame = Utilities.create'Frame'
{
Position = UDim2.new(0, 0, 0, 270);
Size = UDim2.new(1, 0, 1, 0);
Name = "PageFrame";
BackgroundTransparency = 1;
ClipsDescendants = true;
Visible = true;
Selectable = false;
ZIndex = LayoutInfo.BasicLayer;
Parent = Container;
}
local scrollingFrame = Utilities.create'ScrollingFrame'
{
AnchorPoint = Vector2.new(1, 0);
Position = UDim2.new(1, -99, 0, 0);
Size = UDim2.new(0, 491, 1, -270);
Name = "ScrollingFrame";
BackgroundTransparency = 1;
ClipsDescendants = true;
Visible = true;
ScrollingEnabled = false;
ScrollBarThickness = 0;
Selectable = false;
ZIndex = LayoutInfo.BasicLayer;
Parent = frame;
}
ConsoleButtonIndicators.init(Container)
local warningFrame = Utilities.create'Frame'
{
AnchorPoint = Vector2.new(0.5, 0);
Position = UDim2.new(0.5, 0, 0, 0);
Size = UDim2.new(0, LayoutInfo.WarningMaxLength, 1, 0);
Name = "WarningWidget";
BackgroundTransparency = 1;
Visible = true;
Selectable = false;
ZIndex = LayoutInfo.IndicatorLayer;
Parent = Container;
}
local CameraController = require(Modules.LuaApp.Legacy.AvatarEditor.CameraController)(
cameraTweenerObject, LayoutInfo.CameraCenterScreenPosition)
local characterManager = CreateCharacterManager(
{
get = Utilities.httpGet;
post = Utilities.httpPost;
},
characterTemplates,
1
)
local WarningWidget = require(Modules.LuaApp.Legacy.AvatarEditor.WarningWidget)(warningFrame, characterManager, true)
local PageManager = CreatePageManager(
XboxAppState.store:getState().RobloxUser.rbxuid,
scrollingFrame,
characterManager
)
characterManager:setUpdateCameraCallback(CameraController.updateCamera)
local CategoryMenu = CreateCategoryMenu(Container)
local TabList = CreateTabList(Container, PageManager)
local function initStickRotation()
local gamepadInput = Vector2.new(0, 0)
ContextActionService:UnbindCoreAction("StickRotation")
ContextActionService:BindCoreAction("StickRotation", function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Change then
gamepadInput = inputObject.Position or gamepadInput
gamepadInput = Vector2.new(gamepadInput.X, gamepadInput.Y)
if math.abs(gamepadInput.X) > THUMBSTICK_DEADZONE then
rotationInfo.delta = STICK_ROTATION_MULTIPLIER * gamepadInput.X
else
rotationInfo.delta = 0
end
end
end,
false, Enum.KeyCode.Thumbstick2)
end
local function initKeyControls()
toggleButtonDebounce = true
toggleViewDebounce = true
ContextActionService:UnbindCoreAction("KeyControls")
ContextActionService:BindCoreAction("KeyControls", function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
if inputObject.KeyCode == Enum.KeyCode.ButtonSelect and not AppState.Store:getState().FullView then
if toggleButtonDebounce then
toggleButtonDebounce = false
SoundManager:Play('ButtonPress')
AppState.Store:dispatch(ToggleAvatarType())
toggleButtonDebounce = true
end
end
if inputObject.KeyCode == Enum.KeyCode.ButtonR3 then
if toggleViewDebounce then
toggleViewDebounce = false
SoundManager:Play('ScreenChange')
AppState.Store:dispatch(ToggleAvatarEditorFullView())
toggleViewDebounce = true
end
end
end
end,
false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonSelect)
end
local function initMenuActions()
ContextActionService:UnbindCoreAction("AvatarEditorMenu")
ContextActionService:BindCoreAction("AvatarEditorMenu", function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
if AppState.Store:getState().ConsoleMenuLevel > LayoutInfo.ConsoleMenuLevel.CategoryMenu then
local currentMenuLevel = AppState.Store:getState().ConsoleMenuLevel
SoundManager:Play('PopUp')
AppState.Store:dispatch(SetConsoleMenuLevel(currentMenuLevel - 1))
else
-- Back to avatar page in AppShell
AppState.Store:dispatch(ResetCategory())
AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.None))
return Enum.ContextActionResult.Pass
end
end
elseif inputState == Enum.UserInputState.Begin then
if inputObject.KeyCode == Enum.KeyCode.ButtonL2 then
if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then
CategoryMenu:SelectPreviousPage()
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then
TabList:SelectPreviousPage()
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then
PageManager:SelectPreviousPage()
end
elseif inputObject.KeyCode == Enum.KeyCode.ButtonR2 then
if AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.CategoryMenu then
CategoryMenu:SelectNextPage()
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.TabList then
TabList:SelectNextPage()
elseif AppState.Store:getState().ConsoleMenuLevel == LayoutInfo.ConsoleMenuLevel.AssetsPage then
PageManager:SelectNextPage()
end
end
end
end,
false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2)
end
local function updateFullViewCoreAction(fullView)
ContextActionService:UnbindCoreAction("FullView")
if fullView == true then
ContextActionService:BindCoreAction("FullView",
function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
SoundManager:Play('ScreenChange')
AppState.Store:dispatch(ToggleAvatarEditorFullView())
end
return Enum.ContextActionResult.Sink
end
end,
false, Enum.KeyCode.ButtonB, Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonR2)
BackgroundOverlay.Visible = false
elseif fullView == false then
BackgroundOverlay.Visible = true
end
end
local function FocusAll()
initStickRotation()
initKeyControls()
initMenuActions()
CameraController:Focus()
CategoryMenu:Focus()
TabList:Focus()
PageManager:Focus()
WarningWidget:Focus()
if not savedSelectedGuiObject and not AppState.Store:getState().FullView then
AppState.Store:dispatch(SetConsoleMenuLevel(LayoutInfo.ConsoleMenuLevel.CategoryMenu))
elseif savedSelectedGuiObject then
Utilities.setSelectedCoreObject(savedSelectedGuiObject)
end
updateFullViewCoreAction(AppState.Store:getState().FullView)
storeChangedCn = AppState.Store.changed:connect(function(newState, oldState)
if newState.FullView ~= oldState.FullView then
updateFullViewCoreAction(newState.FullView)
if newState.FullView == true then
local selectedCoreObject = Utilities.getSelectedCoreObject()
if selectedCoreObject then
selectedGuiObjectEnterFullView = selectedCoreObject
end
Utilities.setSelectedCoreObject(nil)
else
if selectedGuiObjectEnterFullView then
Utilities.setSelectedCoreObject(selectedGuiObjectEnterFullView)
end
end
end
if not characterEquipped or not characterUpdated then
local didUpdate = false
local didEquip = false
if newState.Character.AvatarType ~= oldState.Character.AvatarType then
didUpdate = true
end
if newState.Character.Assets ~= oldState.Character.Assets then
for assetType, assetList in pairs(oldState.Character.Assets) do
if not newState.Character.Assets[assetType] and assetList then
if next(assetList) ~= nil then
didUpdate = true
didEquip = true
break
end
end
end
if not didUpdate or not didEquip then
for assetType, _ in pairs(newState.Character.Assets) do
if newState.Character.Assets[assetType] ~= oldState.Character.Assets[assetType] then
local addTheseAssets = TableUtilities.ListDifference(
newState.Character.Assets[assetType] or {},
oldState.Character.Assets[assetType] or {})
local removeTheseAssets =
TableUtilities.ListDifference(oldState.Character.Assets[assetType] or {},
newState.Character.Assets[assetType] or {})
if next(addTheseAssets) ~= nil or next(removeTheseAssets) ~= nil then
didUpdate = true
didEquip = true
break
end
end
end
end
end
if newState.Character.BodyColors ~= oldState.Character.BodyColors then
local differentBodyColors =
TableUtilities.TableDifference(newState.Character.BodyColors, oldState.Character.BodyColors)
if next(differentBodyColors) ~= nil then
didUpdate = true
end
end
if newState.Character.Scales ~= oldState.Character.Scales then
local differentScales =
TableUtilities.TableDifference(newState.Character.Scales, oldState.Character.Scales)
if next(differentScales) ~= nil then
didUpdate = true
end
end
if not characterEquipped then
characterEquipped = didEquip
end
if not characterUpdated then
characterUpdated = didUpdate
end
end
end)
scrollingFrameCn = scrollingFrame.Changed:connect(
function(prop)
if prop == 'CanvasPosition' then
PageManager:updateListContent(scrollingFrame.CanvasPosition.Y)
end
end
)
end
local function RemoveFocusAll()
CameraController:RemoveFocus()
PageManager:RemoveFocus()
CategoryMenu:RemoveFocus()
TabList:RemoveFocus()
WarningWidget:RemoveFocus()
Utilities.disconnectEvent(storeChangedCn)
Utilities.disconnectEvent(scrollingFrameCn)
ContextActionService:UnbindCoreAction("FullView")
ContextActionService:UnbindCoreAction("AvatarEditorMenu")
ContextActionService:UnbindCoreAction("StickRotation")
ContextActionService:UnbindCoreAction("KeyControls")
end
function this:Show(parent)
local startCount = showCount
inShow = true
characterUpdated = false
characterEquipped = false
local prevAvatarEditorLoader = avatarEditorLoader
Utilities.fastSpawn(function()
if startCount == showCount then
avatarEditorLoader = LoadingWidget(
{Parent = parent},
{function()
if prevAvatarEditorLoader then
prevAvatarEditorLoader:AwaitFinished()
prevAvatarEditorLoader:Cleanup()
end
if startCount == showCount then
characterManager.initFromServer()
characterManager.show()
end
end}
)
avatarEditorLoader:AwaitFinished()
if startCount == showCount then
avatarEditorLoader:Cleanup()
avatarEditorLoader = nil
end
end
if startCount == showCount then
Container.Parent = parent
if inFocus then
FocusAll()
end
while startCount == showCount do
local deltaT = Utilities.renderWait()
if startCount == showCount then
rotationInfo.rotation = rotationInfo.rotation + deltaT * rotationInfo.delta
characterManager.setRotation(rotationInfo.rotation)
if not lastCharacterSaveTime or tick() - lastCharacterSaveTime > 5 then
characterManager.saveToServer(false)
lastCharacterSaveTime = tick()
end
end
end
end
end)
end
function this:Hide()
inShow = false
showCount = showCount + 1
characterManager.hide()
savedSelectedGuiObject = nil
selectedGuiObjectEnterFullView = nil
Container.Parent = nil
if not avatarEditorLoader then
RemoveFocusAll()
CategoryMenu:Hide()
WarningWidget:Hide()
local savedCharacterUpdated = characterUpdated
local savedCharacterEquipped = characterEquipped
spawn(function()
while savingAvatarMutex do
wait()
end
savingAvatarMutex = true
characterManager.saveToServer(true)
savingAvatarMutex = false
if savedCharacterUpdated then
Utilities.fastSpawn(function()
EventHub:dispatchEvent(EventHub.Notifications["CharacterUpdated"])
end)
end
if savedCharacterEquipped then
Utilities.fastSpawn(function()
EventHub:dispatchEvent(EventHub.Notifications["CharacterEquipped"], AppState.Store:getState().Character.Assets, savedCharacterUpdated)
end)
end
end)
end
end
--Rebind actions, make them in the right order
function this:Focus()
inFocus = true
if inShow and not avatarEditorLoader then
FocusAll()
end
end
function this:RemoveFocus()
inFocus = false
if inShow and not avatarEditorLoader then
savedSelectedGuiObject = Utilities.getSelectedCoreObject()
RemoveFocusAll()
end
end
function this:Destruct()
this:RemoveFocus()
this:Hide()
Container:Destroy()
characterManager.destroy()
AppState:Destruct()
end
return this
end
return createAvatarEditorView
@@ -0,0 +1,706 @@
-- Written by Kip Turner and Bo Zhang, Copyright Roblox 2017
local CoreGui = game:GetService("CoreGui")
local GuiService = game:GetService('GuiService')
local RunService = game:GetService('RunService')
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local PackageData = require(ShellModules:FindFirstChild('PackageData'))
local ScrollingGridModule = require(ShellModules:FindFirstChild('ScrollingGrid'))
local AvatarTile = require(ShellModules:FindFirstChild('AvatarTile'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget'))
local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local HintActionView = require(ShellModules:FindFirstChild('HintActionView'))
local XboxAppState = require(ShellModules:FindFirstChild('AppState'))
local GLOW_BASE_RPM = 2
local GLOW_TOP_RPM = -0.5
local GLOW_TRANSPARENCY = 0.2
local CATALOG_BELOW_POSITION = 320
local function CreateAvatarPane(parent)
local this = {}
local inFocus = false
local isShown = false
local AvatarObjects = {}
local OnGuiServiceChangedConn = nil
local lastParent = parent
local MainContainer = Utility.Create'Frame'
{
Name = 'AvatarPane';
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
Visible = false;
}
local MyAvatarContainer = Utility.Create'Frame'
{
Name = 'MyAvatarContainer';
Size = UDim2.new(0.38,0,1,0);
Position = UDim2.new(0,0,0,0);
BackgroundTransparency = 1;
ClipsDescendants = true;
Parent = MainContainer;
}
local MyNameLabel = Utility.Create'TextLabel'
{
Name = 'MyNameLabel';
Text = '';
Size = UDim2.new(1,0,0,25);
Position = UDim2.new(0,12,0,0);
TextXAlignment = 'Left';
TextColor3 = GlobalSettings.WhiteTextColor;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.SubHeaderSize;
BackgroundTransparency = 1;
Parent = MyAvatarContainer;
};
local ProfileImageContainer = Utility.Create'Frame'
{
Name = 'ProfileImageContainer';
Size = UDim2.new(0.68,0,0.9,-MyNameLabel.Size.Y.Offset);
Position = UDim2.new(0.16, 0, 0.05, MyNameLabel.Size.Y.Offset);
BackgroundTransparency = 1;
Parent = MyAvatarContainer;
}
local ProfileImage = Utility.Create'ImageLabel'
{
Name = 'ProfileImage';
Size = UDim2.new(0,780,0,780);
Position = UDim2.new(0.5, 0, 0.5, 0);
BackgroundTransparency = 1;
ZIndex = 3;
Parent = ProfileImageContainer;
};
local CrossfadeProfileImage = Utility.Create'ImageLabel'
{
Name = 'CrossfadeProfileImage';
Size = UDim2.new(1,0,1,0);
Position = UDim2.new(0, 0, 0, 0);
BackgroundTransparency = 1;
ImageTransparency = 1;
ZIndex = 3;
Parent = ProfileImage;
};
local CharacterGlowBase = Utility.Create'ImageLabel'
{
Name = 'CharacterGlowBase';
Size = UDim2.new(0,1015,0,1002);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowBase.png';
ImageTransparency = GLOW_TRANSPARENCY;
Parent = CrossfadeProfileImage;
AnchorPoint = Vector2.new(0.5, 0.5);
Position = UDim2.new(0.5, 0, 0.5, 0);
};
local CharacterGlowTop = Utility.Create'ImageLabel'
{
Name = 'CharacterGlowTop';
Size = UDim2.new(0,1026,0,1009);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Images/CharacterGlow/CharacterGlowTop.png';
ImageTransparency = GLOW_TRANSPARENCY;
ZIndex = 2;
Parent = CrossfadeProfileImage;
AnchorPoint = Vector2.new(0.5, 0.5);
Position = UDim2.new(0.5, 0, 0.5, 0);
};
local function onSizeChanged()
ProfileImage.Size = Utility.CalculateFill(ProfileImage, ThumbnailLoader.AvatarSizes.Size352x352)
ProfileImage.AnchorPoint = Vector2.new(0.5, 0.5)
ProfileImage.Position = UDim2.new(0.5, 0, 0.5, 0)
end
-- Hint Action View
local hintActionView = HintActionView(nil, "AvatarPaneSelectAction")
hintActionView:SetImage('rbxasset://textures/ui/Shell/ButtonIcons/XButton.png')
hintActionView:SetText(Strings:LocalizedString('EquipWord'))
hintActionView:SetTransparency(1)
hintActionView:SetParent(lastParent.Parent) -- this assumes parent is HubContainer from AppHub.lua
local function UpdateEquipButton(IsOwned, IsWearing)
hintActionView:SetVisibleWithTween( (IsOwned and not IsWearing) and 0 or 1)
end
local SelectableAvatarsContainer = Utility.Create'Frame'
{
Name = 'SelectableAvatarsContainer';
Size = UDim2.new(0.6,0,1,0);
Position = UDim2.new(0.4,0,0,0);
BackgroundTransparency = 1;
Parent = MainContainer;
}
local NoCatalogStatusMessage = Utility.Create'TextLabel'
{
Name = 'NoCatalogStatusMessage';
Text = Strings:LocalizedString('DefaultErrorPhrase');
Size = UDim2.new(0.9,0,1,-125);
Position = UDim2.new(0.05, 0, 0, 0);
TextColor3 = GlobalSettings.GreyTextColor;
TextWrapped = true;
TextTransparency = GlobalSettings.FriendStatusTextTransparency;
Font = GlobalSettings.BoldFont;
FontSize = GlobalSettings.DescriptionSize;
BackgroundTransparency = 1;
Visible = false;
Parent = SelectableAvatarsContainer;
};
local MyCollectionTitle = Utility.Create'TextLabel'
{
Name = 'MyCollectionTitle';
Text = Strings:LocalizedString('AvatarOutfitsTitle');
Size = UDim2.new(1,0,0,40);
TextXAlignment = 'Left';
TextColor3 = GlobalSettings.WhiteTextColor;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.SubHeaderSize;
BackgroundTransparency = 1;
Visible = false;
Parent = SelectableAvatarsContainer;
};
local MyCollectionScroller = ScrollingGridModule()
MyCollectionScroller:SetSize(UDim2.new(1,0,1,-MyCollectionTitle.Size.Y.Offset - 40))
MyCollectionScroller:SetScrollDirection(MyCollectionScroller.Enum.ScrollDirection.Horizontal)
MyCollectionScroller:SetCellSize(Vector2.new(220, 220))
MyCollectionScroller:SetSpacing(Vector2.new(25,25))
MyCollectionScroller:SetPosition(UDim2.new(0,0,0,MyCollectionTitle.Size.Y.Offset))
MyCollectionScroller:SetRowColumnConstraint(1)
-- MyCollectionScroller:SetParent(SelectableAvatarsContainer)
local CatalogTitle = Utility.Create'TextLabel'
{
Name = 'CatalogTitle';
Text = Strings:LocalizedString('AvatarCatalogTitle');
Size = UDim2.new(1,0,0,40);
Position = UDim2.new(0,0,0,0);
TextXAlignment = 'Left';
TextColor3 = GlobalSettings.WhiteTextColor;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.SubHeaderSize;
BackgroundTransparency = 1;
Parent = SelectableAvatarsContainer;
};
local AvatarScroller = ScrollingGridModule()
AvatarScroller:SetSize(UDim2.new(1,0,1,-CatalogTitle.Size.Y.Offset - 40))
AvatarScroller:SetScrollDirection(AvatarScroller.Enum.ScrollDirection.Horizontal)
AvatarScroller:SetCellSize(Vector2.new(220, 220))
AvatarScroller:SetPosition(UDim2.new(0,0,0,40))
AvatarScroller:SetSpacing(Vector2.new(25,25))
AvatarScroller:SetRowColumnConstraint(2)
AvatarScroller:SetParent(SelectableAvatarsContainer)
local function SortMyCollectionScroller()
MyCollectionScroller:SortItems(
function(a, b)
local aObject = a and AvatarObjects[a] and AvatarObjects[a]:GetPackageInfo()
local bObject = b and AvatarObjects[b] and AvatarObjects[b]:GetPackageInfo()
local aIsEquipped = aObject and aObject:IsWearing()
local bIsEquipped = bObject and bObject:IsWearing()
local aName = aObject and aObject:GetName()
local bName = bObject and bObject:GetName()
if aIsEquipped then return true elseif bIsEquipped then return false end
if aName and bName then
return aName < bName
end
return aObject ~= nil
end)
end
--if all avatars owned, MyCollection should be 2 rows and catalog should be hidden
local function CheckAllAvatarsOwned()
if #AvatarScroller.GridItems == 0 then
MyCollectionScroller:SetRowColumnConstraint(2)
CatalogTitle.Visible = false
AvatarScroller.Visible = false
AvatarScroller:SetParent(nil)
else
MyCollectionScroller:SetRowColumnConstraint(1)
CatalogTitle.Visible = true
AvatarScroller.Visible = true
AvatarScroller:SetParent(SelectableAvatarsContainer)
end
end
local function OnAddToMyCollectionScroller()
CatalogTitle.Position = UDim2.new(0,0,0, CATALOG_BELOW_POSITION)
AvatarScroller:SetPosition(UDim2.new(0,0,0,CATALOG_BELOW_POSITION + 40))
AvatarScroller:SetRowColumnConstraint(1)
MyCollectionTitle.Visible = true
MyCollectionScroller:SetParent(SelectableAvatarsContainer)
CheckAllAvatarsOwned()
SortMyCollectionScroller()
end
local LoaderSpinner = nil
local GlobalFadeCount = 1
local function CrossfadeAvatarImage(frontImage, fadeImage, imageSource, duration)
if not imageSource then return end
duration = duration or 0.75
GlobalFadeCount = GlobalFadeCount + 1
local thisFadeCount = GlobalFadeCount
local fadeoutDuration = duration
spawn(function()
local dummyImage = nil
if LoaderSpinner then
LoaderSpinner:Cleanup()
end
--If imageSource if a function, then it's maybe a async call to get new image and we show the loaderspinner.
--Otherwise, if the imageSource is a valid new image, we just do the Crossfade (without the spinner)
if type(imageSource) == 'function' then
local newLoaderSpinner = LoadingWidget({Parent = frontImage, ZIndex = 3}, {
function() dummyImage = imageSource() end
})
LoaderSpinner = newLoaderSpinner
newLoaderSpinner:AwaitFinished()
newLoaderSpinner:Cleanup()
elseif type(imageSource) == 'table' and imageSource.Image then
dummyImage = imageSource
end
if thisFadeCount == GlobalFadeCount then
--Avoid updating with same image
if dummyImage and frontImage and dummyImage.Image ~= frontImage.Image then
local noPreviousImage = (frontImage.Image == "" and fadeImage.Image == "")
if noPreviousImage then
fadeoutDuration = 0
else
fadeImage.Image = frontImage.Image
end
Utility.PropertyTweener(fadeImage, 'ImageTransparency', noPreviousImage and 0 or frontImage.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true)
Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true)
Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, 1, fadeoutDuration, Utility.EaseInOutQuad, true)
frontImage.ImageTransparency = 1
frontImage.Image = dummyImage.Image
Utility.PropertyTweener(frontImage, 'ImageTransparency', frontImage.ImageTransparency, 0, duration, Utility.EaseInOutQuad, true)
Utility.PropertyTweener(CharacterGlowBase, 'ImageTransparency', CharacterGlowBase.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true)
Utility.PropertyTweener(CharacterGlowTop, 'ImageTransparency', CharacterGlowTop.ImageTransparency, GLOW_TRANSPARENCY, duration, Utility.EaseInOutQuad, true)
end
end
end)
end
local function UpdateProfileImage(imageSource)
MyNameLabel.Text = XboxAppState.store:getState().XboxUser.gamertag
CrossfadeAvatarImage(ProfileImage, CrossfadeProfileImage, imageSource)
end
local function onOwnershipChanged(tile, nowOwns)
-- Move purchased packages into my MyCollection
if nowOwns then
local guiObject = tile and tile:GetGuiObject()
if guiObject and AvatarScroller:ContainsItem(guiObject) then
AvatarScroller:RemoveItem(guiObject)
MyCollectionScroller:AddItem(guiObject)
OnAddToMyCollectionScroller()
if inFocus then
GuiService.SelectedCoreObject = guiObject
end
end
end
end
local ownershipChangedCns = {}
local function listenToOwnershipChanged(tile)
local packageInfo = tile and tile:GetPackageInfo()
if packageInfo and not packageInfo:IsOwned() and (packageInfo.OwnershipChanged ~= nil) then
if not ownershipChangedCns[tile] then
ownershipChangedCns[tile] = packageInfo.OwnershipChanged:connect(function(nowOwns)
onOwnershipChanged(tile, nowOwns)
end)
end
end
end
local function removeListenToOwnershipChanged(tile)
if ownershipChangedCns[tile] then
ownershipChangedCns[tile]:disconnect()
ownershipChangedCns[tile] = nil
end
end
local function onEquipChanged()
local selectedObject = GuiService.SelectedCoreObject
if AvatarObjects[selectedObject] then
local packageInfo = AvatarObjects[selectedObject]:GetPackageInfo()
if packageInfo then
UpdateEquipButton(packageInfo:IsOwned(), packageInfo:IsWearing())
end
else
-- user can select avatar tab button
UpdateEquipButton(false, false)
end
SortMyCollectionScroller()
end
local lastSelectedObject = GuiService.SelectedCoreObject
local function OnSelectedCoreObjectChanged()
local selectedObject = GuiService.SelectedCoreObject
if AvatarObjects[lastSelectedObject] then
AvatarObjects[lastSelectedObject]:RemoveFocus()
end
if AvatarObjects[selectedObject] then
AvatarObjects[selectedObject]:Focus()
lastSelectedObject = selectedObject
end
onEquipChanged()
end
function this:GetDefaultSelectableObject()
if MyCollectionScroller:GetFirstVisibleItem() then
return MyCollectionScroller:GetFirstVisibleItem()
end
if AvatarScroller:GetFirstVisibleItem() then
return AvatarScroller:GetFirstVisibleItem()
end
--In case GetFirstVisibleItem breaks
if MyCollectionScroller.GridItems[1] then
return MyCollectionScroller.GridItems[1]
end
if AvatarScroller.GridItems[1] then
return AvatarScroller.GridItems[1]
end
end
local packagesData = nil
local packagesDataConns = {}
local AvatarWebDataInitializeLoader = nil
local function removeListenToPackagesDataUpdate()
for _, conn in pairs(packagesDataConns) do
conn:disconnect()
end
packagesDataConns = {}
end
local function listenToPackagesDataUpdate()
if packagesData and packagesDataConns then
table.insert(packagesDataConns, packagesData.OnDifferentWearing:connect(function(assetId) onEquipChanged() end))
table.insert(packagesDataConns, packagesData.OnDifferentOwned:connect(function(assetId, owned) onEquipChanged() end))
table.insert(packagesDataConns, packagesData.OnProfileImageUpdateEnd:connect(function(newProfileImage)
UpdateProfileImage(newProfileImage)
end))
table.insert(packagesDataConns, packagesData.OnProfileImageUpdateBegin:connect(function()
UpdateProfileImage(function()
return packagesData.OnProfileImageUpdateEnd.wait()
end)
end))
end
end
local function LoadAvatarWebData()
local function clearCatalogPackages()
AvatarScroller:RemoveAllItems()
MyCollectionScroller:RemoveAllItems()
for k,_ in pairs(AvatarObjects) do
AvatarObjects[k] = nil
end
AvatarObjects = {}
end
local function loadCatalogPackages(packages)
clearCatalogPackages()
if packages then
for _, packageInfo in pairs(packages) do
local avatarItemContainer = AvatarTile(packageInfo)
AvatarObjects[avatarItemContainer:GetGuiObject()] = avatarItemContainer
if packageInfo:IsOwned() then
MyCollectionScroller:AddItem(avatarItemContainer:GetGuiObject())
OnAddToMyCollectionScroller()
else
AvatarScroller:AddItem(avatarItemContainer:GetGuiObject())
listenToOwnershipChanged(avatarItemContainer)
end
if isShown then
avatarItemContainer:Show()
end
end
end
end
--Wait until initialize is done
if AvatarWebDataInitializeLoader then return end
if PackageData:HasCachedData() then --Sync process to get and update packagesData, no spinner
local prevVersion = packagesData and packagesData.Version or nil
packagesData = PackageData:GetCachedData()
--If init or packagesData got updated
if not prevVersion or prevVersion ~= packagesData.Version then
SelectableAvatarsContainer.Visible = false
UpdateProfileImage(packagesData.ProfileImage.Data)
loadCatalogPackages(packagesData.Packages.Data)
SelectableAvatarsContainer.Visible = true
removeListenToPackagesDataUpdate()
listenToPackagesDataUpdate()
end
else --Async process to get and update packagesData, show spinner
SelectableAvatarsContainer.Visible = false
local containerSize = SelectableAvatarsContainer.Size
AvatarWebDataInitializeLoader = LoadingWidget(
{Parent = MainContainer, Position = SelectableAvatarsContainer.Position + UDim2.new(containerSize.X.Scale / 2, containerSize.X.Offset / 2, containerSize.Y.Scale / 2, containerSize.Y.Offset / 2)},
{function()
packagesData = PackageData:GetCachedData()
if packagesData and packagesData.Packages then
loadCatalogPackages(packagesData.Packages.Data)
end
end}
)
spawn(function()
NoCatalogStatusMessage.Visible = false
UpdateProfileImage(function()
if AvatarWebDataInitializeLoader then
AvatarWebDataInitializeLoader:AwaitFinished()
end
if packagesData and packagesData.ProfileImage then
return packagesData.ProfileImage.Data
end
end)
AvatarWebDataInitializeLoader:AwaitFinished()
AvatarWebDataInitializeLoader:Cleanup()
AvatarWebDataInitializeLoader = nil
SelectableAvatarsContainer.Visible = true
removeListenToPackagesDataUpdate()
if not packagesData then
NoCatalogStatusMessage.Visible = true
end
--Hook connections if the pane is still visible after loading
if isShown then
listenToPackagesDataUpdate()
end
if inFocus and isShown and GuiService.SelectedCoreObject == nil then
GuiService.SelectedCoreObject = this:GetDefaultSelectableObject()
end
if this.TransitionTweens == nil or #this.TransitionTweens == 0 then
this.TransitionTweens = ScreenManager:FadeInSitu(SelectableAvatarsContainer)
end
end)
end
end
LoadAvatarWebData()
--[[ Public API ]]--
function this:GetName()
return Strings:LocalizedString('AvatarWord')
end
function this:GetAnalyticsInfo()
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarPaneId')}
end
function this:IsFocused()
return inFocus
end
local debounceSelect = false
function this:OnSelectAction()
if debounceSelect then return end
debounceSelect = true
local selectedObject = GuiService.SelectedCoreObject
if selectedObject and AvatarObjects[selectedObject] then
if AvatarObjects[selectedObject]:Select() then
SoundManager:Play('ButtonPress')
end
end
debounceSelect = false
end
function this:OpenEquippedPackage()
if isShown and inFocus then
for _, avatarItemContainer in pairs(AvatarObjects) do
local packageInfo = avatarItemContainer:GetPackageInfo()
if packageInfo then
if packageInfo:IsWearing() then
avatarItemContainer:OnClick()
end
end
end
end
end
--[[ End Public API ]]--
local profileImageChangeCn = nil
function this:Show()
isShown = true
Utility.DisconnectEvent(profileImageChangeCn)
profileImageChangeCn = ProfileImageContainer:GetPropertyChangedSignal('AbsoluteSize'):connect(function()
onSizeChanged()
end)
onSizeChanged()
local lastUpdate = tick()
RunService:BindToRenderStep("UpdateAvatarGlow", Enum.RenderPriority.Camera.Value,
function()
local now = tick()
local delta = now - lastUpdate
CharacterGlowBase.Rotation = CharacterGlowBase.Rotation + delta * GLOW_BASE_RPM * 6 -- 6 = 360 / 60
CharacterGlowTop.Rotation = CharacterGlowTop.Rotation + delta * GLOW_TOP_RPM * 6
lastUpdate = now
end)
if packagesData then
listenToPackagesDataUpdate()
UpdateProfileImage(packagesData.ProfileImage.Data)
end
LoadAvatarWebData()
for _, avatarItemContainer in pairs(AvatarObjects) do
avatarItemContainer:Show()
listenToOwnershipChanged(avatarItemContainer)
local packageInfo = avatarItemContainer and avatarItemContainer:GetPackageInfo()
if packageInfo then
onOwnershipChanged(avatarItemContainer, packageInfo:IsOwned())
end
end
local seenXButtonPressed = false
local seenXSelectedObject = nil
local function onSelectAvatar(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenXButtonPressed = true
seenXSelectedObject = GuiService.SelectedCoreObject
elseif inputState == Enum.UserInputState.End and seenXButtonPressed then
if seenXSelectedObject and seenXSelectedObject == GuiService.SelectedCoreObject then
self:OnSelectAction()
end
seenXButtonPressed = false
seenXSelectedObject = nil
end
end
hintActionView:BindAction(onSelectAvatar, Enum.KeyCode.ButtonX)
self.TransitionTweens = ScreenManager:DefaultFadeIn(MainContainer)
SortMyCollectionScroller()
MainContainer.Parent = lastParent
MainContainer.Visible = true
ScreenManager:PlayDefaultOpenSound()
end
function this:Hide()
isShown = false
MainContainer.Visible = false
removeListenToPackagesDataUpdate()
profileImageChangeCn = Utility.DisconnectEvent(profileImageChangeCn)
RunService:UnbindFromRenderStep("UpdateAvatarGlow")
hintActionView:UnbindAction()
hintActionView:SetTransparency(1)
for _, avatarItemContainer in pairs(AvatarObjects) do
avatarItemContainer:Hide()
removeListenToOwnershipChanged(avatarItemContainer)
end
ScreenManager:DefaultCancelFade(self.TransitionTweens)
self.TransitionTweens = nil
-- Clean out saved selected object so when we tab back
-- we will start with the default selection
lastSelectedObject = nil
self.SavedSelectObject = nil
end
function this:Focus()
inFocus = true
Utility.DisconnectEvent(OnGuiServiceChangedConn)
OnGuiServiceChangedConn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function()
OnSelectedCoreObjectChanged()
end)
if self.SavedSelectObject and self.SavedSelectObject:IsDescendantOf(MainContainer) then
GuiService.SelectedCoreObject = self.SavedSelectObject
else
local defaultSelection = self:GetDefaultSelectableObject()
if defaultSelection then
GuiService.SelectedCoreObject = defaultSelection
end
end
end
function this:RemoveFocus()
inFocus = false
--Remove Focus on AvatarObjects
if AvatarObjects and AvatarObjects[lastSelectedObject] then
AvatarObjects[lastSelectedObject]:RemoveFocus()
end
OnGuiServiceChangedConn = Utility.DisconnectEvent(OnGuiServiceChangedConn)
local selectedObject = GuiService.SelectedCoreObject
if isShown then
if selectedObject and selectedObject:IsDescendantOf(MainContainer) then
self.SavedSelectObject = GuiService.SelectedCoreObject
GuiService.SelectedCoreObject = nil
else
self.SavedSelectObject = lastSelectedObject
end
end
OnSelectedCoreObjectChanged()
end
function this:SetPosition(newPosition)
MainContainer.Position = newPosition
end
function this:SetParent(newParent)
lastParent = newParent
MainContainer.Parent = newParent
end
return this
end
return CreateAvatarPane
@@ -0,0 +1,254 @@
--[[
// AvatarTile.lua
// Created by Kip Turner
// Copyright Roblox 2015
]]
local TextService = game:GetService('TextService')
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
local Http = require(ShellModules:FindFirstChild('Http'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local Errors = require(ShellModules:FindFirstChild('Errors'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay'))
local PurchasePackagePrompt = require(ShellModules:FindFirstChild('PurchasePackagePrompt'))
local BaseTile = require(ShellModules:FindFirstChild('BaseTile'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local function createAvatarInfoContainer(packageInfo)
local this = BaseTile()
local focused = false
local packageName = packageInfo:GetFullName()
local function wearPackageAsync()
if packageInfo:IsOwned() and not packageInfo:IsWearing() then
local result = packageInfo:WearAsync()
if result and result['success'] == true then
this:UpdateEquipButton()
else
local err = Errors.PackageEquip['Default']
ScreenManager:OpenScreen(ErrorOverlayModule(err), false)
end
end
end
local function buyPackageAsync()
local newPurchasePrompt = PurchasePackagePrompt(packageInfo)
ScreenManager:OpenScreen(newPurchasePrompt, false)
newPurchasePrompt:FadeInBackground()
spawn(function()
local didPurchase = newPurchasePrompt:ResultAsync()
if didPurchase then
SoundManager:Play('PurchaseSuccess')
wearPackageAsync()
end
end)
end
local PriceText = Utility.Create'TextLabel'
{
Name = 'PriceText';
Text = '';
Size = UDim2.new(1, 0, 0, 36);
Position = UDim2.new(0, 0, 0, 0);
TextColor3 = GlobalSettings.BlackTextColor;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.DescriptionSize;
BorderSizePixel = 0;
BackgroundColor3 = GlobalSettings.PriceLabelColor;
ZIndex = 2;
Visible = false;
Parent = this.AvatarItemContainer;
};
local PromoText = Utility.Create'TextLabel'
{
Name = 'PromoText';
Text = '';
Size = UDim2.new(1, 0, 0, 36);
Position = UDim2.new(0, 0, 0, 0);
TextColor3 = GlobalSettings.WhiteTextColor;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.DescriptionSize;
BorderSizePixel = 0;
BackgroundColor3 = GlobalSettings.PromoLabelColor;
ZIndex = 2;
Visible = false;
Parent = this.AvatarItemContainer;
};
function this:UpdatePriceText()
local newText = ""
local price = packageInfo:GetRobuxPrice()
if price == 0 then
newText = Strings:LocalizedString('FreeWord')
elseif price then
newText = "R$ " .. Utility.FormatNumberString(price)
end
PriceText.Text = newText
local priceTextSize = TextService:GetTextSize(PriceText.Text, Utility.ConvertFontSizeEnumToInt(PriceText.FontSize), PriceText.Font, Vector2.new())
PriceText.Size = UDim2.new(0, priceTextSize.X + 28, 0, 36)
PriceText.AnchorPoint = Vector2.new(1, 0)
PriceText.Position = UDim2.new(1, -6, 0, 6)
PriceText.Visible = price ~= nil and not packageInfo:IsOwned()
end
function this:UpdatePromoText()
if not packageInfo:IsOwned() then
local saleIdStr = Utility.GetFastVariable("XboxSaleAvatarPackageIds")
if type(saleIdStr) == "string" and saleIdStr ~= '' then
for saleId in string.gmatch(saleIdStr, '([^;]+)') do
if tonumber(saleId) ~= nil and tonumber(saleId) == tonumber(packageInfo:GetAssetId()) then --If the package is on sale
PromoText.Text = "SALE!"
local promoTextSize = TextService:GetTextSize(PromoText.Text, Utility.ConvertFontSizeEnumToInt(PromoText.FontSize), PromoText.Font, Vector2.new())
PromoText.Size = UDim2.new(0, promoTextSize.X + 28, 0, 36)
PromoText.AnchorPoint = Vector2.new(1, 0)
PromoText.Position = UDim2.new(1, -6, 0, 42)
PromoText.Visible = true
break
end
end
end
else
PromoText.Visible = false
end
end
if packageInfo:GetAssetId() then
this:SetImage(Http.GetThumbnailUrlForAsset(packageInfo:GetAssetId()))
else
--TODO: show a no package image?
end
this:ColorizeImage(packageInfo:IsOwned() and 1 or 0, 0)
this:SetPopupText(packageInfo:GetName())
function this:GetAssetId()
return packageInfo:GetAssetId()
end
function this:GetPackageInfo()
return packageInfo
end
function this:UpdateOwnership()
local ownsAsset = packageInfo:IsOwned()
self:SetActive(ownsAsset)
self:ColorizeImage(ownsAsset and 1 or 0, 0)
self:UpdateEquipButton()
self:UpdatePriceText()
--Added for promo label
self:UpdatePromoText()
end
function this:UpdateEquipButton()
self.EquippedCheckmark.Visible = packageInfo:IsWearing()
end
local selectDebounce = false
function this:Select()
if selectDebounce then return false end
local result = false
if packageInfo:IsOwned() and not packageInfo:IsWearing() then
selectDebounce = true
spawn(function()
wearPackageAsync()
selectDebounce = false
end)
result = true
end
return result
end
function this:OnClick()
buyPackageAsync()
end
local isWearingConn = nil
local ownershipChangedCn = nil
local baseShow = this.Show
function this:Show()
baseShow(self)
Utility.DisconnectEvent(isWearingConn)
packageInfo.IsWearingChanged:connect(function()self:UpdateEquipButton() end)
Utility.DisconnectEvent(ownershipChangedCn)
ownershipChangedCn = packageInfo.OwnershipChanged:connect(function()
self:UpdateOwnership()
end)
self:UpdateEquipButton()
self:UpdateOwnership()
end
local baseHide = this.Hide
function this:Hide()
baseHide(self)
isWearingConn = Utility.DisconnectEvent(isWearingConn)
ownershipChangedCn = Utility.DisconnectEvent(ownershipChangedCn)
end
local baseFocus = this.Focus
local avatarItemClickConn = nil
function this:GetAnalyticsInfo()
return
{
[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('AvatarTileId');
AssetId = packageInfo:GetAssetId();
IsOwned = packageInfo:IsOwned();
}
end
function this:Focus()
baseFocus(self)
focused = true
Utility.DisconnectEvent(avatarItemClickConn)
avatarItemClickConn = self.AvatarItemContainer.MouseButton1Click:connect(function()
self:OnClick()
end)
self:UpdateEquipButton()
spawn(function()
wait(0.17)
if focused then
if not packageInfo:IsOwned() then
self:ColorizeImage(1)
end
end
end)
end
local baseRemoveFocus = this.RemoveFocus
function this:RemoveFocus()
baseRemoveFocus(self)
focused = false
avatarItemClickConn = Utility.DisconnectEvent(avatarItemClickConn)
-- Decolorize unowned packages
if not packageInfo:IsOwned() then
self:ColorizeImage(0)
end
end
return this
end
return createAvatarInfoContainer
@@ -0,0 +1,70 @@
-- Starts and manages the background 3D scene
local RobloxGui = game:GetService('CoreGui').RobloxGui
local ShellModules = script.Parent
local ContentProvider = game:GetService('ContentProvider')
local ThirdPartyUserService = nil
pcall(function() ThirdPartyUserService = game:GetService('ThirdPartyUserService') end)
local CameraManager = require(ShellModules.CameraManager)
local EventHub = require(ShellModules.EventHub)
local SoundManager = require(ShellModules.SoundManager)
local Utility = require(ShellModules.Utility)
local SceneManager = {}
-- set up the image before 3D background is displayed
local BackgroundImage = Utility.Create'ImageLabel'
{
Name = 'BackgroundImage';
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 0;
BackgroundColor3 = Color3.new(29/255, 47/255, 61/255);
BorderSizePixel = 0;
Image = 'rbxasset://textures/ui/Shell/Background/Home_screen_01.png';
Parent = RobloxGui;
}
-- We may want to move this to rodux flow later
EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "authSuccessCameraControl", function(isNewLinkedAccount)
CameraManager:DisableCameraControl()
end);
if ThirdPartyUserService then
ThirdPartyUserService.ActiveUserSignedOut:connect(function()
CameraManager:EnableCameraControl()
end)
end
-- start up background music
local backgroundSound = SoundManager:Play('BackgroundLoop', 0.33, true)
if backgroundSound then
local bgmLoopConn = nil
bgmLoopConn = backgroundSound.DidLoop:connect(function(soundId, loopCount)
if loopCount > 0 then
bgmLoopConn = Utility.DisconnectEvent(bgmLoopConn)
if backgroundSound then
SoundManager:TweenSound(backgroundSound, 0.1, 3)
end
end
end)
end
-- Check for 3D Background being loaded, then start it up
local CROSSFADE_DURATION = 1.5
spawn(function()
while ContentProvider.RequestQueueSize > 0 do
wait(0.01)
end
CameraManager:EnableCameraControl()
spawn(function()
CameraManager:CameraMoveToAsync()
end)
Utility.PropertyTweener(BackgroundImage, 'BackgroundTransparency', 0, 1, CROSSFADE_DURATION, Utility.EaseInOutQuad, true)
Utility.PropertyTweener(BackgroundImage, 'ImageTransparency', 0, 1, CROSSFADE_DURATION, Utility.EaseInOutQuad, true, function()
BackgroundImage:Destroy()
end)
end)
return SceneManager
@@ -0,0 +1,135 @@
--[[
// BadgeOverlay.lua
// Displays information for a single badge
// Used by GameDetail and BadgeScreen
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local BaseOverlay = require(ShellModules:FindFirstChild('BaseOverlay'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local createBadgeOverlay = function(badgeData)
local this = BaseOverlay()
local hasBadge = badgeData["IsOwned"]
this:SetImageBackgroundTransparency(0)
this:SetImageBackgroundColor(hasBadge and GlobalSettings.BadgeOwnedColor or GlobalSettings.BadgeOverlayColor)
local badgeImage = Utility.Create'ImageLabel'
{
Name = "BadgeImage";
Size = UDim2.new(0, 394, 0, 394);
BackgroundTransparency = 1;
Image = 'http://www.roblox.com/Thumbs/Asset.ashx?width='..
tostring(250)..'&height='..tostring(250)..'&assetId='..tostring(badgeData.AssetId);
}
badgeImage.Position = UDim2.new(0.5, -197, 0.5, -197)
this:SetImage(badgeImage)
local titleText = Utility.Create'TextLabel'
{
Name = "TitleText";
Size = UDim2.new(0, 0, 0, 0);
Position = UDim2.new(0, this.RightAlign, 0, 88);
BackgroundTransparency = 1;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.HeaderSize;
TextColor3 = GlobalSettings.WhiteTextColor;
Text = badgeData.Name;
TextXAlignment = Enum.TextXAlignment.Left;
Parent = this.Container;
}
--[[ Has Badge ]]--
local hasBadgeContainer = nil
if hasBadge then
hasBadgeContainer = Utility.Create'Frame'
{
Name = "HasBadgeContainer";
Position = UDim2.new(0, titleText.Position.X.Offset, 0, titleText.Position.Y.Offset + 34);
BackgroundTransparency = 1;
Parent = this.Container;
}
local hasBadgeImage = Utility.Create'ImageLabel'
{
Name = "HasBadgeImage";
BackgroundTransparency = 1;
Parent = hasBadgeContainer;
Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png";
Size = UDim2.new(0,35,0,35);
}
hasBadgeContainer.Size = UDim2.new(0, 200, 0, hasBadgeImage.Size.Y.Offset)
local hasBadgeText = Utility.Create'TextLabel'
{
Name = "HasBadgeText";
Size = UDim2.new(0, 0, 0, 0);
Position = UDim2.new(0, hasBadgeImage.Size.X.Offset + 12, 0.5, 0);
BackgroundTransparency = 1;
Font = GlobalSettings.ItalicFont;
FontSize = GlobalSettings.DescriptionSize;
TextColor3 = GlobalSettings.GreenTextColor;
TextXAlignment = Enum.TextXAlignment.Left;
Text = Strings:LocalizedString("HaveBadgeWord");
Parent = hasBadgeContainer;
}
end
--[[ Description ]]--
local descriptionYOffset = hasBadgeContainer and hasBadgeContainer.Position.Y.Offset + hasBadgeContainer.Size.Y.Offset + 10 or
titleText.Position.Y.Offset + 40
local descriptionScrollingTextBox = ScrollingTextBox(UDim2.new(0, 762, 0, 304),
UDim2.new(0, titleText.Position.X.Offset, 0, descriptionYOffset),
this.Container)
descriptionScrollingTextBox:SetText(badgeData.Description)
descriptionScrollingTextBox:SetFontSize(GlobalSettings.TitleSize)
local descriptionFrame = descriptionScrollingTextBox:GetContainer()
local okButton = Utility.Create'TextButton'
{
Name = "OkButton";
Size = UDim2.new(0, 320, 0, 66);
Position = UDim2.new(0, titleText.Position.X.Offset, 1, -66 - 55);
BorderSizePixel = 0;
BackgroundColor3 = GlobalSettings.BlueButtonColor;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.ButtonSize;
TextColor3 = GlobalSettings.TextSelectedColor;
Text = Strings:LocalizedString("OkWord");
Parent = this.Container;
SoundManager:CreateSound('MoveSelection');
}
Utility.ResizeButtonWithText(okButton, okButton, GlobalSettings.TextHorizontalPadding)
--[[ Input Events ]]--
okButton.MouseButton1Click:connect(function()
this:Close()
end)
local baseFocus = this.Focus
function this:Focus()
baseFocus(this)
Utility.SetSelectedCoreObject(okButton)
end
function this:GetAnalyticsInfo()
return
{
[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BadgeOverlayId');
AssetId = badgeData.AssetId;
}
end
return this
end
return createBadgeOverlay
@@ -0,0 +1,124 @@
--[[
// BadgeScreen.lua
// Displays a 2xN grid of badges for a game
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
local BadgeOverlayModule = require(ShellModules:FindFirstChild('BadgeOverlay'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local ScrollingGrid = require(ShellModules:FindFirstChild('ScrollingGrid'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local PopupText = require(ShellModules:FindFirstChild('PopupText'))
local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
local createBadgeScreen = function(badgeData)
local this = BaseScreen()
local ROWS = 2
-- columns is dynamic
local BadgeContainer = this.Container
this:SetTitle(Strings:LocalizedString("GameBadgesTitle"))
local defaultSelection = nil
-- create grid
local BadgeScrollGrid = ScrollingGrid()
BadgeScrollGrid:SetPosition(UDim2.new(0, 0, 0.5 - (0.57 / 2), 0))
BadgeScrollGrid:SetSize(UDim2.new(1, 0, 0, 570))
BadgeScrollGrid:SetScrollDirection(BadgeScrollGrid.Enum.ScrollDirection.Horizontal)
BadgeScrollGrid:SetParent(BadgeContainer)
BadgeScrollGrid:SetClipping(false)
BadgeScrollGrid:SetCellSize(Vector2.new(276, 276))
BadgeScrollGrid:SetSpacing(Vector2.new(18, 18))
local checkmarkImage = Utility.Create'ImageLabel'
{
Name = "CheckMarkImage";
BackgroundTransparency = 1;
Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png";
Size = UDim2.new(0,35,0,35);
ZIndex = 2;
}
local function connectImageInput(image, data)
image.MouseButton1Click:connect(function()
-- Do not play sound because we are opening a screen here
ScreenManager:OpenScreen(BadgeOverlayModule(data), false)
end)
end
local baseItem = Utility.Create'TextButton'
{
Name = "BadgeImage";
BorderSizePixel = 0;
BackgroundTransparency = 0.2;
BackgroundColor3 = Color3.new(64/255, 81/255, 93/255);
Text = "";
ZIndex = 2;
SoundManager:CreateSound('MoveSelection');
AssetManager.CreateShadow(1);
}
local badgeIcon = Utility.Create'ImageLabel'
{
Name = "Thumb";
Size = UDim2.new(0, 228, 0, 228);
Position = UDim2.new(0.5, -228/2, 0.5, -228/2);
BackgroundTransparency = 1;
Image = "";
ZIndex = 2;
Parent = baseItem;
}
for i = 1, #badgeData do
local data = badgeData[i]
local item = baseItem:Clone()
local thumb = item:FindFirstChild("Thumb")
if thumb then
local thumbLoader = ThumbnailLoader:Create(thumb, data.AssetId,
ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.Icon)
spawn(function()
thumbLoader:LoadAsync()
end)
end
local hasBadge = data["IsOwned"]
if hasBadge then
item.BackgroundColor3 = GlobalSettings.BadgeOwnedColor;
item.BackgroundTransparency = 0;
--
local check = checkmarkImage:Clone()
check.Position = UDim2.new(1, -check.Size.X.Offset - 8, 0, 8)
check.Parent = item
end
item.Name = tostring(i)
BadgeScrollGrid:AddItem(item)
connectImageInput(item, data)
PopupText(item, data["Name"])
if not defaultSelection then
defaultSelection = item
end
end
--[[ Public API ]]--
--Override
function this:GetDefaultSelectionObject()
return defaultSelection
end
return this
end
return createBadgeScreen
@@ -0,0 +1,185 @@
--[[
// BadgeSort.lua
// Creates a badge sort for a game
// Handles the following for badges
// Displays 2x2 of badges on game details page
// Displays individual information about each badge (overlay)
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
local BadgeOverlayModule = require(ShellModules:FindFirstChild('BadgeOverlay'))
local EventHub = require(ShellModules:FindFirstChild('EventHub'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local PopupText = require(ShellModules:FindFirstChild('PopupText'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader'))
local CreateBadgeSort = function(placeName, size, position, parent)
local this = {}
local badgeData = nil
local margin = 14
local imageSize = (size.Y.Offset - margin) / 2
local gridImages = {}
local GRID_SIZE = 4
--[[ Game Details Grid ]]--
local container = Utility.Create'Frame'
{
Name = "ImageContainer";
Size = size;
Position = position;
BackgroundTransparency = 1;
Parent = parent;
}
-- create 2x2 preview grid
local index = 1
for i = 1, GRID_SIZE/2 do
for j = 1, GRID_SIZE/2 do
local image = Utility.Create'TextButton'
{
Name = tostring(index);
Size = UDim2.new(0, imageSize, 0, imageSize);
Position = UDim2.new(0, (i - 1) * imageSize + (i - 1) * margin, 0, (j - 1) * imageSize + (j - 1) * margin);
BackgroundTransparency = GlobalSettings.FriendStatusTextTransparency;
BackgroundColor3 = GlobalSettings.BadgeFrameColor;
BorderSizePixel = 0;
ZIndex = 2;
Text = "";
ClipsDescendants = true;
Parent = container;
SoundManager:CreateSound('MoveSelection');
AssetManager.CreateShadow(1);
}
local thumb = Utility.Create'ImageLabel'
{
Name = "Thumb";
Size = UDim2.new(0, 228, 0, 228);
Position = UDim2.new(0.5, -228/2, 0.5, -228/2);
BackgroundTransparency = 1;
Image = "";
ZIndex = 2;
Parent = image;
}
gridImages[index] = image
index = index + 1
end
end
-- more button visible when #badges > 4
local moreBadgesButton = Utility.Create'ImageButton'
{
Name = "MoreBadgesButton";
BackgroundTransparency = 1;
Visible = false;
Parent = container;
Image = "rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png";
Size = UDim2.new(0,108,0,50);
ZIndex = 2;
SoundManager:CreateSound('MoveSelection');
}
moreBadgesButton.Position = UDim2.new(1, - moreBadgesButton.AbsoluteSize.x, 1, 12)
local function updateMoreButton(isSelected)
moreBadgesButton.Image = isSelected and 'rbxasset://textures/ui/Shell/Buttons/MoreButtonSelected@1080.png'
or 'rbxasset://textures/ui/Shell/Buttons/MoreButton@1080.png'
end
moreBadgesButton.SelectionGained:connect(function()
updateMoreButton(true)
end)
moreBadgesButton.SelectionLost:connect(function()
updateMoreButton(false)
end)
local checkmarkImage = Utility.Create'ImageLabel'
{
Name = "CheckMarkImage";
BackgroundTransparency = 1;
Image = "rbxasset://textures/ui/Shell/Icons/Checkmark@1080.png";
Size = UDim2.new(0,35,0,35);
ZIndex = 2;
}
local function setBadgeData()
if not badgeData then
Utility.DebugLog("BadgeSort: failed to set badge data because data is nil.")
return
end
--
for i = 1, #gridImages do
if badgeData[i] then
local data = badgeData[i]
local thumb = gridImages[i]:FindFirstChild("Thumb")
if thumb then
local thumbLoader = ThumbnailLoader:Create(thumb, data.AssetId,
ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.Icon)
spawn(function()
thumbLoader:LoadAsync()
end)
end
local hasBadge = data["IsOwned"]
if hasBadge then
gridImages[i].BackgroundColor3 = GlobalSettings.BadgeOwnedColor
gridImages[i].BackgroundTransparency = 0
--
local check = checkmarkImage:Clone()
check.Position = UDim2.new(1, -check.Size.X.Offset - 8, 0, 8)
check.Parent = gridImages[i]
end
--
gridImages[i].MouseButton1Click:connect(function()
ScreenManager:OpenScreen(BadgeOverlayModule(data), false)
end)
-- connect popup text
PopupText(gridImages[i], data["Name"])
end
end
end
--[[ Input Events ]]--
moreBadgesButton.MouseButton1Click:connect(function()
EventHub:dispatchEvent(EventHub.Notifications["OpenBadgeScreen"], badgeData, placeName)
end)
--[[ Public API ]]--
function this:GetContainer()
return container
end
function this:GetDefaultSelection()
return gridImages and gridImages[1] or nil
end
function this:Initialize(data)
if not badgeData then
badgeData = data
setBadgeData()
if #data > GRID_SIZE then
moreBadgesButton.Visible = true
end
end
end
function this:Destroy()
container:Destroy()
gridImages = nil
end
return this
end
return CreateBadgeSort
@@ -0,0 +1,475 @@
--[[
// BaseCarouselScreen.lua
// Creates a base screen for a carousel view
// To be used for game genre and search screens
// Creates a Play and Favorite Button, details view (votes, description), and
// a carousel view
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
local Errors = require(ShellModules:FindFirstChild('Errors'))
local ErrorOverlayModule = require(ShellModules:FindFirstChild('ErrorOverlay'))
local GameData = require(ShellModules:FindFirstChild('GameData'))
local GameJoinModule = require(ShellModules:FindFirstChild('GameJoin'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local LoadingWidget = require(ShellModules:FindFirstChild('LoadingWidget'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
local CarouselView = require(ShellModules:FindFirstChild('CarouselView'))
local CarouselController = require(ShellModules:FindFirstChild('CarouselController'))
local VoteFrame = require(ShellModules:FindFirstChild('VoteFrame'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local FFlagXboxSponsoredGames = settings():GetFFlag("XboxSponsoredGames")
local function CreateBaseCarouselScreen()
local this = BaseScreen()
local newGameSelectedCn = nil
local dataModelViewChangedCn = nil
local canJoinGame = true
local returnedFromGame = true
-- we need to move ZIndex up because of drop shadows
local BASE_ZINDEX = 2
local playButtonColor = GlobalSettings.GreenButtonColor
local playButtonSelectedColor = GlobalSettings.GreenSelectedButtonColor
local favoriteButtonColor = GlobalSettings.GreyButtonColor
local favoriteSelectedButtonColor = GlobalSettings.GreySelectedButtonColor
local buttonTextColor = GlobalSettings.WhiteTextColor
local buttonSelectedTextColor = GlobalSettings.TextSelectedColor
local viewContainer = Utility.Create'Frame'
{
Name = "ViewContainer";
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
Parent = this.Container;
}
this.ViewContainer = viewContainer
local myCarouselView = CarouselView()
myCarouselView:SetSize(UDim2.new(0, 1724, 0, 450))
myCarouselView:SetPosition(UDim2.new(0, 0, 0, 240))
myCarouselView:SetPadding(18)
myCarouselView:SetItemSizePercentOfContainer(2/3)
myCarouselView:SetParent(viewContainer)
local myCarouselController = CarouselController(myCarouselView)
local playButton = Utility.Create'ImageButton'
{
Name = "PlayButton";
Size = UDim2.new(0, 228, 0, 72);
Position = UDim2.new(0, 0, 1, -77);
BackgroundTransparency = 1;
ImageColor3 = playButtonColor;
Image = GlobalSettings.RoundCornerButtonImage;
ScaleType = Enum.ScaleType.Slice;
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28));
ZIndex = BASE_ZINDEX;
Parent = viewContainer;
SoundManager:CreateSound('MoveSelection');
AssetManager.CreateShadow(1);
}
local playText = Utility.Create'TextLabel'
{
Name = "PlayText";
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
Text = Strings:LocalizedString("PlayWord");
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.ButtonSize;
TextColor3 = buttonTextColor;
ZIndex = BASE_ZINDEX;
Parent = playButton;
}
Utility.ResizeButtonWithText(playButton, playText, GlobalSettings.TextHorizontalPadding)
local favoriteButton = Utility.Create'ImageButton'
{
Name = "FavoriteButton";
Position = UDim2.new(0, playButton.Size.X.Offset + 10, 1, -77);
Size = UDim2.new(0, 228, 0, 72);
BackgroundTransparency = 1;
ImageColor3 = favoriteButtonColor;
Image = GlobalSettings.RoundCornerButtonImage;
ScaleType = Enum.ScaleType.Slice;
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28));
ZIndex = BASE_ZINDEX;
Parent = viewContainer;
SoundManager:CreateSound('MoveSelection');
AssetManager.CreateShadow(1);
}
local favoriteText = Utility.Create'TextLabel'
{
Name = "FavoriteText";
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
Text = Strings:LocalizedString("FavoriteWord");
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.ButtonSize;
TextColor3 = buttonTextColor;
ZIndex = 2;
Parent = favoriteButton;
}
local favoriteStarImage = Utility.Create'ImageLabel'
{
Name = "FavoriteStarImage";
Size = UDim2.new(0, 32, 0, 31);
Position = UDim2.new(0, 16, 0.5, -31/2);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Icons/FavoriteStar@1080.png';
Visible = false;
ZIndex = BASE_ZINDEX;
Parent = favoriteButton;
}
--Make it big enough to hold the star and text
Utility.ResizeButtonWithDynamicText(favoriteButton, favoriteText, {Strings:LocalizedString("FavoritedWord")},
GlobalSettings.TextHorizontalPadding + (favoriteStarImage.Position.X.Offset + favoriteStarImage.Size.X.Offset + 12) / 2)
-- begin game details content
local gameDetailsContainer = Utility.Create'Frame'
{
Name = "GameDetailsContainer";
Size = UDim2.new(0, 0, 0, 0);
Position = UDim2.new(0, 18, 0, 732);
BackgroundTransparency = 1;
Parent = viewContainer;
}
local gameTitle = Utility.Create'TextLabel'
{
Name = "GameTitleLabel";
Size = UDim2.new(0, 0, 0, 0);
Position = UDim2.new(0, 0, 0, 0);
BackgroundTransparency = 1;
Text = "";
TextColor3 = GlobalSettings.WhiteTextColor;
TextXAlignment = Enum.TextXAlignment.Left;
Font = GlobalSettings.LightFont;
FontSize = GlobalSettings.HeaderSize;
Parent = gameDetailsContainer;
}
local voteFrame = VoteFrame(gameDetailsContainer, UDim2.new(0, 38, 0, 46))
local voteFrameContainer = voteFrame:GetContainer()
local thumbsUpImage = Utility.Create'ImageLabel'
{
Name = "ThumbsUpImage";
Size = UDim2.new(0, 28, 0, 28);
Position = UDim2.new(0, 0, 0, voteFrameContainer.Position.Y.Offset + voteFrameContainer.Size.Y.Offset - 28);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsUpIcon@1080.png';
Parent = gameDetailsContainer;
}
local thumbsDownImage = Utility.Create'ImageLabel'
{
Name = "ThumbsDownImage";
Size = UDim2.new(0, 28, 0, 28);
Position = UDim2.new(0, voteFrameContainer.Position.X.Offset + voteFrameContainer.Size.X.Offset + 10, 0, voteFrameContainer.Position.Y.Offset);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Icons/ThumbsDownIcon@1080.png';
Parent = gameDetailsContainer;
}
local separatorDot = Utility.Create'ImageLabel'
{
Name = "SeparatorDot";
Size = UDim2.new(0, 10, 0, 10);
Position = UDim2.new(0, thumbsDownImage.Position.X.Offset + thumbsDownImage.Size.X.Offset + 32, 0, voteFrameContainer.Position.Y.Offset + (voteFrameContainer.Size.Y.Offset/2) - (10/2));
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Icons/SeparatorDot@1080.png';
Parent = gameDetailsContainer;
}
local creatorIcon = Utility.Create'ImageLabel'
{
Name = "CreatorIcon";
Size = UDim2.new(0, 24, 0, 24);
Position = UDim2.new(0, separatorDot.Position.X.Offset + separatorDot.Size.X.Offset + 32, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 12);
BackgroundTransparency = 1;
Image = 'rbxasset://textures/ui/Shell/Icons/RobloxIcon24.png';
Parent = gameDetailsContainer;
}
local creatorName = Utility.Create'TextLabel'
{
Name = "CreatorName";
Size = UDim2.new(0, 0, 0, 0);
Position = UDim2.new(0, creatorIcon.Position.X.Offset + creatorIcon.Size.X.Offset + 8, 0, separatorDot.Position.Y.Offset + separatorDot.Size.Y.Offset/2 - 2);
BackgroundTransparency = 1;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.DescriptionSize;
TextColor3 = GlobalSettings.LightGreyTextColor;
TextXAlignment = Enum.TextXAlignment.Left;
Text = "";
Parent = gameDetailsContainer;
}
local descriptionText = Utility.Create'TextLabel'
{
Name = "DescriptionText";
Size = UDim2.new(0, 850, 0, 64);
Position = UDim2.new(0, gameTitle.Position.X.Offset, 0, voteFrameContainer.Position.Y.Offset + voteFrameContainer.Size.Y.Offset + 20);
BackgroundTransparency = 1;
Text = "";
TextColor3 = GlobalSettings.LightGreyTextColor;
TextXAlignment = Enum.TextXAlignment.Left;
TextYAlignment = Enum.TextYAlignment.Top;
Font = GlobalSettings.LightFont;
TextWrapped = true;
FontSize = GlobalSettings.DescriptionSize;
Parent = gameDetailsContainer;
}
local noResultsText = Utility.Create'TextLabel'
{
Name = "noResultsText";
Size = UDim2.new(0, 0, 0, 0);
Position = UDim2.new(0.5, 0, 0.5, 0);
BackgroundTransparency = 1;
Text = Strings:LocalizedString("NoGamesPhrase");
TextColor3 = GlobalSettings.LightGreyTextColor;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.MediumFontSize;
Visible = false;
Parent = this.Container;
}
-- Selection overrides
playButton.NextSelectionLeft = playButton
favoriteButton.NextSelectionRight = favoriteButton
playButton.SelectionGained:connect(function()
playButton.ImageColor3 = playButtonSelectedColor
playText.TextColor3 = buttonSelectedTextColor
end)
playButton.SelectionLost:connect(function()
playButton.ImageColor3 = playButtonColor
playText.TextColor3 = buttonTextColor
end)
favoriteButton.SelectionGained:connect(function()
favoriteButton.ImageColor3 = favoriteSelectedButtonColor
favoriteText.TextColor3 = buttonSelectedTextColor
end)
favoriteButton.SelectionLost:connect(function()
favoriteButton.ImageColor3 = favoriteButtonColor
favoriteText.TextColor3 = buttonTextColor
end)
local function setIsFavorited(isFavorited)
if isFavorited == true then
favoriteStarImage.Visible = true
favoriteText.Position = UDim2.new(0, favoriteStarImage.Position.X.Offset + favoriteStarImage.Size.X.Offset + 12, 0, 0)
favoriteText.Text = Strings:LocalizedString("FavoritedWord")
favoriteText.TextXAlignment = Enum.TextXAlignment.Left
else
favoriteStarImage.Visible = false
favoriteText.Position = UDim2.new(0, 0, 0, 0)
favoriteText.Text = Strings:LocalizedString("FavoriteWord")
favoriteText.TextXAlignment = Enum.TextXAlignment.Center
end
end
local function setVoteView(voteData)
local upVotes = voteData and voteData.UpVotes or 0
local downVotes = voteData and voteData.DownVotes or 0
if upVotes == 0 and downVotes == 0 then
voteFrame:SetPercentFilled(nil)
else
voteFrame:SetPercentFilled(upVotes / (upVotes + downVotes))
end
end
local GameSelectedConns = {}
local onNewGameSelected = nil
local function ClearGameSelectedView()
--Disconnect Events
Utility.DisconnectEvents(GameSelectedConns)
GameSelectedConns = {}
gameTitle.Text = ""
creatorName.Text = ""
descriptionText.Text = ""
setVoteView()
setIsFavorited()
end
onNewGameSelected = function(placeId)
Utility.DisconnectEvents(GameSelectedConns)
GameSelectedConns = {}
local data = GameData:GetGameData(placeId)
if data then
--Use signals to make sure that these fetched data corresponds to the game we focus on
table.insert(GameSelectedConns, data.OnGetVoteDataEnd:
connect(function(voteData) setVoteView(voteData)
end))
table.insert(GameSelectedConns, data.OnGetGameDetailsEnd:
connect(function(gameData)
descriptionText.Text = gameData.Description or ""
setIsFavorited(gameData.IsFavorited)
end))
gameTitle.Text = data.Name
creatorName.Text = data.CreatorName
descriptionText.Text = data.Description or ""
setVoteView(data.VoteData)
setIsFavorited(data.IsFavorited)
spawn(function()
if not data.VoteData then
data:GetVoteDataAsync()
end
if not data.Description or data.IsFavorited == nil then
data:GetGameDetailsAsync()
end
end)
else
ClearGameSelectedView()
end
end
playButton.MouseButton1Click:connect(function()
SoundManager:Play('ButtonPress')
local currentFocusGameData = myCarouselController:GetCurrentFocusGameData()
local placeId
if FFlagXboxSponsoredGames then
placeId = currentFocusGameData and currentFocusGameData.placeId or nil
else
placeId = currentFocusGameData and currentFocusGameData or nil
end
local data = GameData:GetGameData(placeId)
if data then
if canJoinGame and returnedFromGame then
canJoinGame = false
--Should never happen, as all game in carousel inits with CreatorUserId
if not data.CreatorUserId then
data:GetGameDetailsAsync()
end
GameJoinModule:StartGame(GameJoinModule.JoinType.Normal, data.PlaceId, data.CreatorUserId)
canJoinGame = true
end
end
end)
favoriteButton.MouseButton1Click:connect(function()
SoundManager:Play('ButtonPress')
local currentFocusGameData = myCarouselController:GetCurrentFocusGameData()
local placeId
if FFlagXboxSponsoredGames then
placeId = currentFocusGameData and currentFocusGameData.placeId or nil
else
placeId = currentFocusGameData and currentFocusGameData or nil
end
local data = GameData:GetGameData(placeId)
if data then
local success, reason = data:PostFavoriteAsync()
if success then
setIsFavorited(data.IsFavorited)
elseif reason then
local err = Errors.Favorite[reason]
ScreenManager:OpenScreen(ErrorOverlayModule(err), false)
end
end
end)
function this:LoadGameCollection(gameCollection)
viewContainer.Visible = false
noResultsText.Visible = false
myCarouselView:SetParent(nil)
spawn(function()
local loader = LoadingWidget(
{ Parent = this.Container },
{
function()
myCarouselController:InitializeAsync(gameCollection)
end
}
)
loader:AwaitFinished()
loader:Cleanup()
loader = nil
myCarouselView:SetParent(viewContainer)
if this:IsFocused() then
if myCarouselController:HasResults() then
viewContainer.Visible = true
Utility.SetSelectedCoreObject(myCarouselView:GetFocusItem())
else
noResultsText.Visible = true
end
end
end)
end
-- Override Base Functions
function this:GetDefaultSelectionObject()
return myCarouselView:GetFocusItem()
end
function this:GetAnalyticsInfo()
return
{
[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseCarouselScreenId');
Title = this:GetTitle();
}
end
local baseFocus = this.Focus
function this:Focus()
baseFocus(self)
myCarouselView:Focus()
if PlatformService then
dataModelViewChangedCn = PlatformService.ViewChanged:connect(function(viewType)
-- return from game debounce
if viewType == 0 then
returnedFromGame = false
wait(1)
returnedFromGame = true
end
end)
end
newGameSelectedCn = myCarouselController.NewItemSelected:connect(onNewGameSelected)
local currentFocusGameData = myCarouselController:GetCurrentFocusGameData()
local placeId
if FFlagXboxSponsoredGames then
placeId = currentFocusGameData and currentFocusGameData.placeId or nil
else
placeId = currentFocusGameData and currentFocusGameData or nil
end
onNewGameSelected(placeId)
myCarouselController:Connect()
end
local baseRemoveFocus = this.RemoveFocus
function this:RemoveFocus()
baseRemoveFocus(self)
dataModelViewChangedCn = Utility.DisconnectEvent(dataModelViewChangedCn)
newGameSelectedCn = Utility.DisconnectEvent(newGameSelectedCn)
myCarouselController:Disconnect()
Utility.DisconnectEvents(GameSelectedConns)
GameSelectedConns = {}
end
return this
end
return CreateBaseCarouselScreen
@@ -0,0 +1,160 @@
--[[
// BaseOverlay.lua
// Implements a base overlay for overlay screens.
// Any other overlay classes should require this module
// first, then implement its own logic
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local GuiService = game:GetService('GuiService')
local ContextActionService = game:GetService("ContextActionService")
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local FADE_TIME = 0.25
local MOCKUP_WIDTH = 1920
local MOCKUP_HEIGHT = 1080
local CONTENT_WIDTH = 1920
local CONTENT_HEIGHT = 690
local CONTENT_POSITION = Vector2.new(0, 225)
local createBaseOverlay = function()
local this = {}
local OVERLAY_TRANSPARENCY = GlobalSettings.ModalBackgroundTransparency
this.RightAlign = 776
local modalOverlay = Utility.Create'Frame'
{
Name = "ModalOverlay";
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
BackgroundColor3 = Color3.new();
BorderSizePixel = 0;
}
local container = Utility.Create'Frame'
{
Name = "Container";
Size = UDim2.new(CONTENT_WIDTH/MOCKUP_WIDTH, 0, CONTENT_HEIGHT/MOCKUP_HEIGHT, 0);
Position = UDim2.new(CONTENT_POSITION.x/MOCKUP_WIDTH, 0, CONTENT_POSITION.y/MOCKUP_HEIGHT, 0);
BorderSizePixel = 0;
BackgroundColor3 = GlobalSettings.OverlayColor;
Parent = modalOverlay;
}
local imageContainer = Utility.Create'Frame'
{
Name = "ImageContainer";
Size = UDim2.new(0, 576, 0, 642);
Position = UDim2.new(0, 100, 0.5, -321);
BorderSizePixel = 0;
BackgroundTransparency = 1;
BackgroundColor3 = Color3.new();
ZIndex = 2;
Parent = container;
}
local imageDropShadow = AssetManager.CreateShadow(1)
imageDropShadow.ImageTransparency = 1
imageDropShadow.Parent = imageContainer
this.Container = container
--[[ Public API ]]--
function this:GetAnalyticsInfo()
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseOverlayId')}
end
function this:SetImageBackgroundTransparency(value)
imageContainer.BackgroundTransparency = value
-- if the image has any transparency we don't show drop shadow since it will clip
if value > 0 then
imageDropShadow.ImageTransparency = 1
else
imageDropShadow.ImageTransparency = 0
end
end
function this:SetImageBackgroundColor(value)
imageContainer.BackgroundColor3 = value
end
function this:SetImage(guiImage)
guiImage.AnchorPoint = Vector2.new(0.5, 0.5);
guiImage.Position = UDim2.new(0.5, 0, 0.5, 0);
guiImage.ZIndex = imageContainer.ZIndex
guiImage.Parent = imageContainer
end
function this:GetOverlaySound()
return 'OverlayOpen'
end
function this:GetPriority()
return GlobalSettings.OverlayPriority
end
function this:Show()
modalOverlay.Parent = ScreenManager:GetScreenGuiByPriority(self:GetPriority())
local overlayTweenIn = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency",
1, OVERLAY_TRANSPARENCY, FADE_TIME, Utility.EaseInOutQuad, nil)
SoundManager:Play(self:GetOverlaySound())
-- Show the modalOverlay when we are shown
modalOverlay.Visible = true
end
function this:Hide()
local overlayTweenOut = Utility.PropertyTweener(modalOverlay, "BackgroundTransparency",
OVERLAY_TRANSPARENCY, 1, FADE_TIME, Utility.EaseInOutQuad, true,
function()
modalOverlay:Destroy()
end)
container.Parent = nil
container:Destroy()
end
function this:Focus()
ContextActionService:BindCoreAction("CloseOverlay",
function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
ScreenManager:CloseCurrent()
end
end,
false, Enum.KeyCode.ButtonB)
GuiService:AddSelectionParent("Overlay", container)
-- Don't show overlays when not focused
modalOverlay.Visible = true
end
function this:RemoveFocus()
ContextActionService:UnbindCoreAction("CloseOverlay")
GuiService:RemoveSelectionGroup("Overlay")
-- Don't show overlays when not focused
modalOverlay.Visible = false
end
function this:Close()
if ScreenManager:GetTopScreen() == self then
SoundManager:Play('ButtonPress')
ScreenManager:CloseCurrent()
return true
end
return false
end
return this
end
return createBaseOverlay
@@ -0,0 +1,187 @@
--[[
// BaseScreen.lua
// Creates a base screen with breadcrumbs and title. Do not use for a pane/tab
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local ContextActionService = game:GetService("ContextActionService")
local GuiService = game:GetService('GuiService')
local ScreenManager = require(ShellModules:FindFirstChild('ScreenManager'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local function createBaseScreen(usingMVC)
local this = {}
local inFocus = false
local isShown = false
local defaultSelectionObject = nil
local lastParent = nil
local BackText = ""
this.view = nil
this.fixPosition = false
function this:GetBackText()
return BackText
end
local function GetView()
if not this.view then
local Templates = ShellModules:FindFirstChild("Templates")
local BaseScreen = require(Templates:FindFirstChild('BaseScreen'))
this.view = BaseScreen(this)
end
return this.view
end
if not usingMVC then
this.Container = GetView().Container
end
local function CloseScreen()
if inFocus then
ScreenManager:CloseCurrent()
end
end
--[[ Public API ]]--
function this:GetView()
return GetView()
end
function this:GetTitle()
return GetView().TitleText.Text
end
function this:SetTitle(newTitle)
GetView().TitleText.Text = newTitle
end
function this:SetTitleZIndex(newZIndex)
-- this needs some explantion...
-- we recently changed the way z ordering works. In the old system if you had a text label and a image at the
-- same zindex the text would render on top of the image. This is no longer the case. So in some cases
-- we need to render this text last, but as it's a base class, it becomes hard to reorder the child add order.
-- Instead I'm opting to allow changing the zindex
GetView().TitleText.ZIndex = newZIndex
end
function this:SetBackText(newText)
BackText = newText
GetView():SetBackText(BackText)
end
function this:GetDefaultSelectionObject()
return defaultSelectionObject
end
function this:Destroy()
GetView().Container:Destroy()
end
--[[ Public API - Screen Management ]]--
function this:SetPosition(newPosition)
GetView().Container.Position = newPosition
end
function this:SetParent(newParent)
lastParent = newParent
end
function this:GetName()
return GetView().TitleText.Text
end
function this:GetAnalyticsInfo()
return {[Analytics.WidgetNames('WidgetId')] = GetView().TitleText.Text}
end
function this:IsFocused()
return inFocus
end
-- View stuff --
function this:ReloadView(newView)
local currentView = this.view
local currentViewIsShown = isShown
local currentViewIsFocused = inFocus
if currentView then
if currentViewIsFocused then
currentView:RemoveFocus()
end
if currentViewIsShown then
currentView:Hide()
end
end
if newView then
if currentViewIsShown then
newView:Show()
end
if currentViewIsFocused then
newView:Focus()
end
end
this.view = newView
end
function this:OnBackButtonClick()
CloseScreen()
end
----------------
function this:Show()
isShown = true
local prevScreen = ScreenManager:GetScreenBelow(self)
if prevScreen and prevScreen.GetName then
self:SetBackText(prevScreen:GetName())
else
self:SetBackText(Strings:LocalizedString("BackWord"))
end
GetView().Container.Parent = lastParent
ScreenManager:DefaultCancelFade(self.TransitionTweens)
if self.fixPosition then
self.TransitionTweens = ScreenManager:FadeInSitu(GetView().Container)
else
self.TransitionTweens = ScreenManager:DefaultFadeIn(GetView().Container)
end
ScreenManager:PlayDefaultOpenSound()
end
function this:Hide()
isShown = false
GetView().Container.Parent = nil
ScreenManager:DefaultCancelFade(self.TransitionTweens)
self.TransitionTweens = nil
end
function this:Focus()
inFocus = true
if self.SavedSelectedObject and self.SavedSelectedObject:IsDescendantOf(GetView().Container) then
Utility.SetSelectedCoreObject(self.SavedSelectedObject)
else
Utility.SetSelectedCoreObject(self:GetDefaultSelectionObject())
end
ContextActionService:BindCoreAction("ReturnFromScreen",
function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
CloseScreen()
end
end,
false, Enum.KeyCode.ButtonB)
end
function this:RemoveFocus()
inFocus = false
local selectedObject = GuiService.SelectedCoreObject
if selectedObject and selectedObject:IsDescendantOf(GetView().Container) then
self.SavedSelectedObject = selectedObject
Utility.SetSelectedCoreObject(nil)
end
ContextActionService:UnbindCoreAction("ReturnFromScreen")
end
return this
end
return createBaseScreen
@@ -0,0 +1,211 @@
--[[
// BaseSignInScreen.lua
// Creates a base screen to be used for account linking and sign in
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
local BaseScreen = require(ShellModules:FindFirstChild('BaseScreen'))
local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local Strings = require(ShellModules:FindFirstChild('LocalizedStrings'))
local TextBox = require(ShellModules:FindFirstChild('TextBox'))
local Utility = require(ShellModules:FindFirstChild('Utility'))
local ScrollingTextBox = require(ShellModules:FindFirstChild('ScrollingTextBox'))
local PlatformService = nil
pcall(function() PlatformService = game:GetService('PlatformService') end)
local TERMS_OF_SERVICE_URI = "https://en.help.roblox.com/hc/en-us/articles/205358110"
local function createBaseAccountScreen()
local this = BaseScreen()
local DefaultButtonColor = GlobalSettings.GreyButtonColor
local SelectedButtonColor = GlobalSettings.GreySelectedButtonColor
local DefaultButtonTextColor = GlobalSettings.WhiteTextColor
local SelectedButtonTextColor = GlobalSettings.TextSelectedColor
local ScreenDivide = Utility.Create'Frame'
{
Name = "ScreenDivide";
Size = UDim2.new(0, 2, 0, 610);
Position = UDim2.new(0, 822, 0.5, -305);
BorderSizePixel = 0;
BackgroundColor3 = GlobalSettings.PageDivideColor;
Parent = this.Container;
}
local TermsOfServiceText = Utility.Create'TextLabel'
{
Name = "TermsOfServiceText";
Size = UDim2.new(0, 740, 0, 128);
Position = UDim2.new(0, 16, 0, 835);
BackgroundTransparency = 1;
Font = GlobalSettings.LightFont;
FontSize = GlobalSettings.ButtonSize;
TextColor3 = GlobalSettings.WhiteTextColor;
TextXAlignment = Enum.TextXAlignment.Left;
TextYAlignment = Enum.TextYAlignment.Top;
TextWrapped = true;
Text = Strings:LocalizedString("ToSInfoLinkPhrase");
Parent = this.Container;
}
local UsernameObject = TextBox(UDim2.new(0, 656, 0, 84))
UsernameObject:SetPosition(UDim2.new(0, ScreenDivide.Position.X.Offset + ScreenDivide.Size.X.Offset + 86, 0, 334))
UsernameObject:SetSpacing(Vector2.new(20, 0))
UsernameObject:SetParent(this.Container)
UsernameObject:SetClipsDescendants(true)
local UsernameTextBox = UsernameObject:GetTextBox()
local UsernameSelection = UsernameObject:GetContainer()
this.UsernameObject = UsernameObject
this.UsernameTextBox = UsernameTextBox
this.UsernameSelection = UsernameSelection
local PasswordObject = TextBox(UDim2.new(0, 656, 0, 84))
PasswordObject:SetPosition(UDim2.new(0, ScreenDivide.Position.X.Offset + ScreenDivide.Size.X.Offset + 86, 0, 484))
PasswordObject:SetSpacing(Vector2.new(20, 0))
PasswordObject:SetParent(this.Container)
PasswordObject:SetClipsDescendants(true)
local PasswordTextBox = PasswordObject:GetTextBox()
local PasswordSelection = PasswordObject:GetContainer()
this.PasswordObject = PasswordObject
this.PasswordTextBox = PasswordTextBox
this.PasswordSelection = PasswordSelection
local function CreateSlicedTextButton(name, text, position)
name = name or ""
local newButton = Utility.Create'ImageButton'
{
Name = name .. "Button";
Size = UDim2.new(0, 376, 0, 64);
Position = position or UDim2.new();
BackgroundTransparency = 1;
ImageColor3 = DefaultButtonColor;
Image = GlobalSettings.RoundCornerButtonImage;
ScaleType = Enum.ScaleType.Slice;
SliceCenter = Rect.new(Vector2.new(4, 4), Vector2.new(28, 28));
ZIndex = 2;
Parent = this.Container;
SoundManager:CreateSound('MoveSelection');
AssetManager.CreateShadow(1)
}
local newText = Utility.Create'TextLabel'
{
Name = name .. "Text";
Size = UDim2.new(1, 0, 1, 0);
BackgroundTransparency = 1;
Font = GlobalSettings.RegularFont;
FontSize = GlobalSettings.ButtonSize;
TextColor3 = DefaultButtonTextColor;
Text = text;
ZIndex = 2;
Parent = newButton;
}
newButton.SelectionGained:connect(function()
newButton.ImageColor3 = SelectedButtonColor
newText.TextColor3 = SelectedButtonTextColor
end)
newButton.SelectionLost:connect(function()
newButton.ImageColor3 = DefaultButtonColor
newText.TextColor3 = DefaultButtonTextColor
end)
return newButton, newText
end
local function TryLaunchUri(uri)
local success, msg = pcall(function()
assert(not UserSettings().GameSettings:InStudioMode() or game:GetService('UserInputService'):GetPlatform() == Enum.Platform.Windows, "Can't use in studio")
PlatformService:LaunchPlatformUri(uri)
end)
if not success then
Utility.DebugLog(string.format("PlatformService:LaunchPlatformUri failed to launch uri: %s, for reason: %s", uri, msg))
end
end
local SignInButton, SignInText = CreateSlicedTextButton("SignIn",
Strings:LocalizedString("SignInPhrase"),
UDim2.new(0, PasswordSelection.Position.X.Offset, 0,
PasswordSelection.Position.Y.Offset + PasswordSelection.Size.Y.Offset + 66))
Utility.ResizeButtonWithText(SignInButton, SignInText, GlobalSettings.TextHorizontalPadding)
this.SignInButton = SignInButton
local tosButtonY = SignInButton.Position.Y.Offset -- + SignInButton.Size.Y.Offset + 30
local ToSButton, ToSText = CreateSlicedTextButton("ToS",
Strings:LocalizedString("ToSPhrase"),
UDim2.new(0, SignInButton.Position.X.Offset + SignInButton.Size.X.Offset + 10,
0, tosButtonY))
ToSButton.Size = UDim2.new(0, 270, 0, ToSButton.Size.Y.Offset)
Utility.ResizeButtonWithText(ToSButton, ToSText, GlobalSettings.TextHorizontalPadding)
local tosButtonLastPress = tick() - 1
ToSButton.MouseButton1Click:connect(function()
if tick() - tosButtonLastPress < 1 then return end
tosButtonLastPress = tick()
TryLaunchUri(TERMS_OF_SERVICE_URI)
end)
--[[
local PrivacyButton, PrivacyText = CreateSlicedTextButton("Privacy",
Strings:LocalizedString("PrivacyPhrase"),
UDim2.new(0, ToSButton.Position.X.Offset + ToSButton.Size.X.Offset + 5,
0, tosButtonY))
PrivacyButton.MouseButton1Click:connect(function()
TryLaunchUri("http://www.roblox.com/info/privacy")
end)
--]]
-- Override selection - issue with selections remembering their last selection, so in some cases
-- the password selection become unselectable. I've talk to Ben about this and we're going to fix it
-- TODO: Remove this when selection memory is fixed
UsernameSelection.NextSelectionDown = PasswordSelection
SignInButton.NextSelectionUp = PasswordSelection
local DescriptionText = ScrollingTextBox(UDim2.new(0, 740, 0, 460), UDim2.new(0, 16, 0, 334), this.Container)
DescriptionText:SetFont(GlobalSettings.RegularFont)
DescriptionText:SetFontSize(GlobalSettings.TitleSize)
DescriptionText.OnSelectableChanged:connect(function(value)
local descriptionSelectionObject = DescriptionText:GetSelectableObject()
if value == true then
descriptionSelectionObject.NextSelectionRight = SignInButton
descriptionSelectionObject.NextSelectionLeft = descriptionSelectionObject
descriptionSelectionObject.NextSelectionUp = descriptionSelectionObject
descriptionSelectionObject.NextSelectionDown = descriptionSelectionObject
else
descriptionSelectionObject.NextSelectionRight = nil
descriptionSelectionObject.NextSelectionLeft = nil
descriptionSelectionObject.NextSelectionUp = nil
descriptionSelectionObject.NextSelectionDown = nil
end
end)
--[[ Public API ]]--
--Override
function this:GetDefaultSelectionObject()
return UsernameSelection
end
function this:SetDescriptionText(newText)
DescriptionText:SetText(newText)
end
function this:SetButtonText(newText)
SignInText.Text = newText
end
return this
end
return createBaseAccountScreen
@@ -0,0 +1,136 @@
--[[
// BaseTile.lua
// Created by Kip Turner
// Copyright Roblox 2015
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
local SoundManager = require(ShellModules:FindFirstChild('SoundManager'))
local AssetManager = require(ShellModules:FindFirstChild('AssetManager'))
local PopupText = require(ShellModules:FindFirstChild('PopupText'))
local Analytics = require(ShellModules:FindFirstChild('Analytics'))
local ACTIVE_AVATAR_BACKGROUND_COLOR = Color3.new(45/255, 96/255, 128/255)
local INACTIVE_AVATAR_BACKGROUND_COLOR = Color3.new(39/255, 69/255, 82/255) --Color3.new(106/255, 120/255, 129/255)
local function createBaseTileContainer()
local this = {}
this.focused = false
this.active = false
local avatarItemContainer = Utility.Create'ImageButton'
{
Name = 'AvatarItemContainer';
Size = UDim2.new(0,220,0,220);
BorderSizePixel = 0;
BackgroundTransparency = 0;
BackgroundColor3 = this.active and ACTIVE_AVATAR_BACKGROUND_COLOR or INACTIVE_AVATAR_BACKGROUND_COLOR;
AutoButtonColor = false;
ClipsDescendants = true;
ZIndex = 2;
AssetManager.CreateShadow(1);
SoundManager:CreateSound('MoveSelection');
}
local myPopText = PopupText(avatarItemContainer, '')
myPopText:SetZIndex(3)
local avatarImage = Utility.Create'ImageLabel'
{
Name = "AvatarImage";
Size = UDim2.new(1, 0, 1, 0);
Position = UDim2.new(0, 0, 0, 0);
BackgroundTransparency = 1;
ZIndex = 2;
Parent = avatarItemContainer;
}
local equippedCheckmark = Utility.Create'ImageLabel'
{
Name = "EquippedCheckmark";
Size = UDim2.new(1, 0, 1, 0);
Position = UDim2.new(0, 0, 0, 0);
BackgroundTransparency = 1;
ZIndex = 3;
Visible = false;
Image = 'rbxasset://textures/ui/Shell/Icons/EquippedOverlay.png';
Parent = avatarItemContainer;
}
this.AvatarItemContainer = avatarItemContainer
this.AvatarImage = avatarImage
this.EquippedCheckmark = equippedCheckmark
local function colorizeImage(newColor, duration)
duration = duration or 0.2
Utility.PropertyTweener(avatarImage, 'ImageColor3', avatarImage.ImageColor3.r, newColor, duration,
function(...) local scalar = Utility.EaseOutQuad(...) return Color3.new(scalar, scalar, scalar) end, true)
end
function this:UpdateEquipButton()
end
function this:ColorizeImage(...)
colorizeImage(...)
end
function this:SetPopupText(newText)
myPopText:SetText(newText)
end
function this:SetImage(imgUrl)
avatarImage.Image = imgUrl
end
function this:GetGuiObject()
return avatarItemContainer
end
function this:GetAnalyticsInfo()
return {[Analytics.WidgetNames('WidgetId')] = Analytics.WidgetNames('BaseTileId')}
end
function this:GetPackageInfo()
end
function this:OnClick()
end
function this:SetActive(isActive)
self.active = isActive
avatarItemContainer.BackgroundColor3 = self.active and ACTIVE_AVATAR_BACKGROUND_COLOR or INACTIVE_AVATAR_BACKGROUND_COLOR;
end
function this:Select()
end
function this:Show()
end
function this:Hide()
end
function this:Focus()
self.focused = true
end
function this:RemoveFocus()
self.focused = false
end
return this
end
return createBaseTileContainer
@@ -0,0 +1,267 @@
--[[
// BatchThumbnailLoader.lua
// Creates a batch thumbnail loader object that handles the loading
// of mutiple thumb nails.
// Thumbnails may not yet be generated, so this will retry and
// assign the final thumbnail
]]
local CoreGui = game:GetService("CoreGui")
local ContentProvider = game:GetService('ContentProvider')
local Modules = CoreGui.RobloxGui.Modules
local Http = require(Modules.Shell.Http)
local LoadingWidget = require(Modules.Shell.LoadingWidget)
local Utility = require(Modules.Shell.Utility)
local RETRIES = 6
local FADE_IN_TIME = 0.25
local TEMPLATE_DECAL = Instance.new("Decal")
local BATCH_LIMIT = 30
local function preloadThumbnailAsync(assetId)
TEMPLATE_DECAL.Texture = assetId
ContentProvider:PreloadAsync({ TEMPLATE_DECAL })
end
local BatchThumbnailLoader = {}
--[[ Sizes ]]--
BatchThumbnailLoader.Sizes =
{
Small = Vector2.new(100, 100);
Medium = Vector2.new(250, 250);
Large = Vector2.new(576, 324);
}
function BatchThumbnailLoader:Init()
self._active = true
self.isLoading = false
self.isRequesting = false
self.imageObjects = {}
self.loadingProps = {}
self.newRequests = nil
self.requests = nil
self.uriMap = {}
self.loaders = {}
self.activeTry = 0
end
local function createThumbnailKey(token, size)
return tostring(token)..tostring(size)
end
function BatchThumbnailLoader:CreateThumbnail(imageObject, token, size, showSpinner, fadeImage, spinnerProperties)
spinnerProperties = spinnerProperties or {}
if showSpinner == nil then
showSpinner = true
end
if fadeImage == nil then
fadeImage = true
end
local requestLoad = false
local thumbnailKey = createThumbnailKey(token, size)
if not self._active then
BatchThumbnailLoader:Init()
end
if self.isLoading == false then
requestLoad = true
if self.newRequests == nil then
self.newRequests = {}
end
end
if self.newRequests[size] == nil then
self.newRequests[size] = {}
end
table.insert(self.newRequests[size], token)
if self.imageObjects[thumbnailKey] == nil then
self.imageObjects[thumbnailKey] = {}
end
self.imageObjects[thumbnailKey][imageObject] = imageObject
--Create the loading spinner.
self.loadingProps[imageObject] = {}
self.loadingProps[imageObject].showSpinner = showSpinner
self.loadingProps[imageObject].fadeImage = fadeImage
self.loadingProps[imageObject].spinnerProperties = spinnerProperties
if self.uriMap[thumbnailKey] ~= nil then
spawn(function()
preloadThumbnailAsync(self.uriMap[thumbnailKey])
BatchThumbnailLoader:_loadImage(imageObject, self.uriMap[thumbnailKey])
end)
return
else
BatchThumbnailLoader:_loadImage(imageObject, nil)
end
if requestLoad then
spawn(function()
BatchThumbnailLoader:BatchLoadThumbnailsAsync()
end)
end
end
function BatchThumbnailLoader:_loadImage(imageObject, imageUri)
local loaded = imageUri ~= nil
if imageObject == nil then
return
end
imageObject.Image = ""
if self.loadingProps[imageObject].fadeImage then
local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 1, 0,
Utility.EaseInOutQuad, true, nil)
end
if not loaded then
if self.loadingProps[imageObject].showSpinner then
self.loadingProps[imageObject].spinnerProperties['Parent'] = self.loadingProps[imageObject].spinnerProperties['Parent']
or imageObject
self.loaders[imageObject] = LoadingWidget(self.loadingProps[imageObject].spinnerProperties, nil, true)
end
else
if self.loaders[imageObject] then
self.loaders[imageObject]:Cleanup()
self.loaders[imageObject] = nil
end
imageObject.Image = imageUri
if self.loadingProps[imageObject].fadeImage then
local tween = Utility.PropertyTweener(imageObject, "ImageTransparency", 1, 0, FADE_IN_TIME,
Utility.EaseInOutQuad, true, nil)
end
end
end
function BatchThumbnailLoader:_loadThumbnails(tokens, size)
local batchTokens;
local reloadBatchList = {}
local batchCount = 0
while #tokens > 0 do
batchCount = batchCount + 1
batchTokens = {}
if #tokens <= BATCH_LIMIT then
batchTokens = tokens
tokens = {}
else
for _=1, BATCH_LIMIT do
local newToken = table.remove(tokens)
table.insert(batchTokens, newToken)
end
end
local result = Http.GetAssetThumbnailBatchAsync(batchTokens, size.x, size.y)
if result then
for i,v in ipairs(result) do
local token = batchTokens[i]
local isFinal = v["final"]
if isFinal then
local uri = v["url"]
local thumbnailKey = createThumbnailKey(token, size)
self.uriMap[thumbnailKey] = uri
for _,imageObj in pairs(self.imageObjects[thumbnailKey]) do
spawn(function()
preloadThumbnailAsync(self.uriMap[thumbnailKey])
BatchThumbnailLoader:_loadImage(imageObj, self.uriMap[thumbnailKey])
end)
end
else
table.insert(reloadBatchList, token)
end
end
else
for _,token in ipairs(batchTokens) do
table.insert(reloadBatchList, token)
end
end
end
return reloadBatchList
end
function BatchThumbnailLoader:_batchLoadThumbnails(batchList)
if batchList == nil then
return nil
end
local requireReload = false
local newBatchList = {};
for size, tokens in pairs(batchList) do
local reloadBatchList = BatchThumbnailLoader:_loadThumbnails(tokens, size)
if #reloadBatchList ~= 0 then
if newBatchList[size] == nil then
newBatchList[size] = {}
end
requireReload = true
newBatchList[size] = reloadBatchList
end
end
if not requireReload then
return nil
end
return newBatchList
end
-- marge new array into old array
local function mergeArray(newArray, oldArray)
local mergedArray = {}
if oldArray ~= nil then
for size, tokens in pairs(oldArray) do
mergedArray[size] = {}
for _,token in ipairs(tokens) do
table.insert(mergedArray[size], token)
end
end
end
if newArray ~= nil then
for size, tokens in pairs(newArray) do
if mergedArray[size] == nil then
mergedArray[size] = {}
end
for _,token in ipairs(tokens) do
table.insert(mergedArray[size], token)
end
end
end
return mergedArray
end
function BatchThumbnailLoader:_tryBatchLoadThumbnails(startTime)
local activeTry = startTime
local tryCount = 1
--retries until the max number of attempts. Or if this is still the most recent batch.
while tryCount <= RETRIES and activeTry == self.activeTry do
self.isRequesting = true
self.requests = BatchThumbnailLoader:_batchLoadThumbnails(self.requests)
self.isRequesting = false
if self.requests == nil then
break
end
tryCount = tryCount + 1
wait(tryCount ^ 2)
end
end
function BatchThumbnailLoader:BatchLoadThumbnailsAsync()
repeat
wait(0.1)
until self.isRequesting == false and self.isLoading == false
self.isLoading = true
if self.newRequests == nil then
-- No new requests.
self.isLoading = false
return
end
--Merge the new list of tokens to load.
self.requests = mergeArray(self.newRequests, self.requests)
self.newRequests = nil
self.isLoading = false
if self.requests ~= nil then
self.activeTry = tick()
BatchThumbnailLoader:_tryBatchLoadThumbnails(self.activeTry)
end
end
function BatchThumbnailLoader:SetTransparency(value, imageObject)
if self.loaders[imageObject] ~= nil then
self.loaders[imageObject]:SetTransparency(value)
end
end
return BatchThumbnailLoader
@@ -0,0 +1,48 @@
local function CreateCacheData(data, version, getRefreshIntervalFunc, refreshFunc)
local this = {}
this.Data = data
this.Version = version
this.GetRefreshIntervalFunc = getRefreshIntervalFunc
this.RefreshFunc = refreshFunc
local refreshDebounce = false
--Use new data to update, usually we do this at intervals in BG
function this:Update(newCached)
if newCached and newCached.Version then
if self.Version <= newCached.Version then
self.Version = newCached.Version
self.Data = newCached.Data
end
end
end
--Refresh the data, usually we will call the Refresh when some data is required
function this:Refresh(forceRefresh)
if self.RefreshFunc and type(self.RefreshFunc) == 'function' then
while refreshDebounce do
wait()
end
--Get RefreshInterval
local RefreshInterval = nil
if self.GetRefreshIntervalFunc and type(self.GetRefreshIntervalFunc) == 'function' then
RefreshInterval = tonumber(self.GetRefreshIntervalFunc())
end
if forceRefresh or not self.Version or not RefreshInterval or (tick() - self.Version >= RefreshInterval) then
refreshDebounce = true
--RefreshFunc will update data, and return whether the refresh was valid
if self.RefreshFunc(self.Data) then
self.Version = tick()
else --this refresh fails, we needs to reset Version to ensure next RefreshFunc call
self.Version = nil
end
refreshDebounce = false
end
end
end
return this
end
return CreateCacheData
@@ -0,0 +1,404 @@
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
local RunService = game:GetService("RunService")
local UserInputService = game:GetService('UserInputService')
local Lighting = game:GetService('Lighting')
local BackgroundTintColor = Color3.new(0.0784, 0.1686, 0.2353)
local ColorCorrection = Utility.Create'ColorCorrectionEffect'
{
Brightness = 0.3;
Contrast = 0.5;
Saturation = -1;
TintColor = BackgroundTintColor;
Enabled = true;
Parent = Lighting;
}
local Blur = Utility.Create'BlurEffect'
{
Size = 10;
Enabled = true;
Parent = Lighting;
}
local Bloom = Utility.Create'BloomEffect'
{
Intensity = 0.05;
Size = 24;
Threshold = 0.95;
Enabled = true;
Parent = Lighting;
}
local function GetCameraParts(model)
local parts = {}
for i, part in pairs(model:GetChildren()) do
parts[tonumber(part.Name:sub(4, -1))] = part
part.Transparency = 1
end
return parts
end
local myCameraSets = nil
local function GetCameraSets()
if myCameraSets == nil then
myCameraSets = {}
for k, f in pairs(workspace:WaitForChild("Cameras"):GetChildren()) do
myCameraSets[f.Name] = GetCameraParts(f)
end
end
return myCameraSets
end
local function onCameraChanged()
if workspace.CurrentCamera then
workspace.CurrentCamera.CameraType = 'Scriptable'
end
end
workspace:GetPropertyChangedSignal('CurrentCamera'):connect(function()
onCameraChanged()
end)
onCameraChanged()
local cameraMoveCn = nil
local gamepadInput = Vector2.new(0, 0)
local CameraManager = {}
function CameraManager:EnableCameraControl()
cameraMoveCn = Utility.DisconnectEvent(cameraMoveCn)
cameraMoveCn = UserInputService.InputChanged:connect(function(input)
if input.KeyCode == Enum.KeyCode.Thumbstick2 then
gamepadInput = input.Position or gamepadInput
gamepadInput = Vector2.new(gamepadInput.X, gamepadInput.Y)
end
end)
end
function CameraManager:DisableCameraControl()
cameraMoveCn = Utility.DisconnectEvent(cameraMoveCn)
gamepadInput = Vector2.new(0, 0)
end
local getGamepadInputCFrame;
do
local gamepadInputLerping = Vector2.new(0, 0)
local timestamp0 = tick()
function getGamepadInputCFrame()
local timestamp1 = tick()
local deltaTime = timestamp1 - timestamp0
timestamp0 = timestamp1
local unit = 0.125 ^ deltaTime
gamepadInputLerping = gamepadInputLerping * unit + gamepadInput * (1 - unit)
return CFrame.new(
gamepadInputLerping.X/8, gamepadInputLerping.Y/8, 0) * CFrame.Angles(0, -gamepadInputLerping.X / 12, 0) * CFrame.Angles(gamepadInputLerping.Y / 12, 0, 0)
end
end
local defaultCFrame = CFrame.new();
local function defaultGetFrameInfo()
return {
CFrame = defaultCFrame;
Contrast = 0.5;
Saturation = -1;
TintColor = BackgroundTintColor;
BlurSize = 10;
}
end
local getFrameInfo = defaultGetFrameInfo
local function startEternalRenderStep()
RunService:BindToRenderStep("CameraScriptCutsceneLerp", Enum.RenderPriority.Camera.Value,
function()
local info = getFrameInfo()
workspace.CurrentCamera.CoordinateFrame = info.CFrame
ColorCorrection.Contrast = info.Contrast
ColorCorrection.Saturation = info.Saturation
ColorCorrection.TintColor = info.TintColor
Blur.Size = info.BlurSize
end
)
end
local function transitionToCameraAnimator(cameraAnimator, transitionDuration, targetBrightness, targetBlurEnabled, targetBloomEnabled)
Utility.PropertyTweener(ColorCorrection, "Brightness", ColorCorrection.Brightness,
-1, transitionDuration, Utility.EaseInOutQuad, true,
function()
getFrameInfo = cameraAnimator:get_getFrameInfo()
Blur.Enabled = targetBlurEnabled
Bloom.Enabled = targetBloomEnabled
Utility.PropertyTweener(ColorCorrection, "Brightness", ColorCorrection.Brightness,
targetBrightness, transitionDuration, Utility.EaseInOutQuad, true)
end)
end
local function CFrameBezierLerp(cframes, t)
local cframes2 = {}
for i = 1, #cframes - 1 do
cframes2[i] = cframes[i]:lerp(cframes[i + 1], t)
end
if #cframes2 == 1 then
return cframes2[1]
end
return CFrameBezierLerp(cframes2, t)
end
local function getCFrameList(cameras)
local cframes = {}
for i = 1, #cameras do
cframes[i] = cameras[i].CFrame
end
return cframes
end
local function CameraSeriesAnimator(cameraSeries, length)
local timestamp0 = 0
local cameraIndex = 0
local cframes = nil
local isRunning = false
local tweenTable = {}
local animationInfo = {
Contrast = -1;
BlurSize = 50;
}
local function myGetFrameInfo()
local t = Utility.Clamp(0, 1, (tick() - timestamp0) / length)
return {
CFrame = CFrameBezierLerp(cframes, t) * getGamepadInputCFrame();
Contrast = animationInfo.Contrast;
Saturation = -1;
TintColor = BackgroundTintColor;
BlurSize = animationInfo.BlurSize;
}
end
local function advance()
if not isRunning then
return
end
cameraIndex = cameraIndex + 1
if cameraIndex > #cameraSeries then
cameraIndex = 1
end
cameraSeries[cameraIndex].init()
cframes = getCFrameList(cameraSeries[cameraIndex].cameras)
timestamp0 = tick()
end
runCameraSeries = function(transitionDuration)
local timePassed = tick() - timestamp0
for i = #tweenTable, 1, -1 do
if tweenTable[i]:IsFinished() then
table.remove(tweenTable, i)
end
end
if timePassed >= length then
local newTween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, 0.5, transitionDuration, Utility.EaseInOutQuad, true)
table.insert(tweenTable, newTween)
newTween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 10, transitionDuration, Utility.EaseInOutQuad, true)
table.insert(tweenTable, newTween)
advance()
elseif timePassed >= length - transitionDuration and #tweenTable == 0 then
local newTween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, -1, transitionDuration, Utility.EaseInOutQuad, true)
table.insert(tweenTable, newTween)
newTween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 50, transitionDuration, Utility.EaseInOutQuad, true)
table.insert(tweenTable, newTween)
end
end
local function startCameraSeries()
local tween = Utility.PropertyTweener(animationInfo, "Contrast", animationInfo.Contrast, 0.5, 4, Utility.EaseInOutQuad, true)
table.insert(tweenTable, tween)
tween = Utility.PropertyTweener(animationInfo, "BlurSize", animationInfo.BlurSize, 10, 4, Utility.EaseInOutQuad, true)
table.insert(tweenTable, tween)
advance()
end
local this = {}
function this:Start()
isRunning = true
startCameraSeries()
RunService:BindToRenderStep("runCameraSeries", Enum.RenderPriority.Camera.Value + 1, function()
runCameraSeries(1.7)
end)
end
function this:Stop()
isRunning = false
for i = #tweenTable, 1, -1 do
local tween = table.remove(tweenTable, i)
tween:Finish()
end
animationInfo = {
Contrast = 0.5;
BlurSize = 10;
}
RunService:UnbindFromRenderStep("runCameraSeries")
end
function this:get_getFrameInfo()
return myGetFrameInfo
end
return this
end
local function CameraZoomAnimator(cframes, length, pathFunc)
local timestamp0 = 0
local this = {}
function this:Reset()
timestamp0 = tick()
end
function this:Update(inCframes, length)
cframes = inCframes
timestamp0 = tick()
end
local function myGetFrameInfo()
local t = Utility.Clamp(0, 1, (tick() - timestamp0) / length)
return {
CFrame = CFrameBezierLerp(cframes, pathFunc(t));
Contrast = 0.35;
Saturation = 0.175;
TintColor = Color3.fromRGB(255, 255, 255);
BlurSize = 1;
}
end
function this:get_getFrameInfo()
return myGetFrameInfo
end
return this
end
pcall(function()
local function recurse(model)
local children = model:GetChildren()
for i = 1, #children do
local child = children[i]
if child:IsA("BasePart") then
child.Locked = true
child.Anchored = true
child.CanCollide = false
end
recurse(child)
end
end
recurse(workspace)
end)
local ZoneManager = require(script.Parent:WaitForChild("CameraManagerModules"):WaitForChild("ZoneManager"))
local avatarEditorCameraAnimator = nil
local flythroughAnimator = nil
local avatarEditorZoomCFrames = {
CFrame.new(
10.2426682, 5.1197648, -30.9536419,
-0.946675897, 0.123298854, -0.297661126,
0.0000000, 0.92387563, 0.382692933,
0.322187454, 0.36228618, -0.874610782),
CFrame.new(
12.50625, 4.83650, -24.764325,
-0.94241035, 0.0557777137, -0.329775006,
0.0000000, 0.98599577, 0.166770056,
0.334458828, 0.157165825, -0.92921263)
}
function CameraManager:CameraMoveToAsync()
local cameraSets = GetCameraSets()
startEternalRenderStep();
flythroughAnimator = CameraSeriesAnimator({
{
init = function() ZoneManager:SetZone("City") end;
cameras = cameraSets["City"]
},
{
init = function() ZoneManager:SetZone("Space") end;
cameras = cameraSets["Space"]
},
{
init = function() ZoneManager:SetZone("Volcano") end;
cameras = cameraSets["Volcano"]
},
}, 60)
avatarEditorCameraAnimator = CameraZoomAnimator(avatarEditorZoomCFrames, 1.0, function(t)
t = Utility.Clamp(0, 1, t)
return t * (2 - t)
end)
flythroughAnimator:Start()
transitionToCameraAnimator(flythroughAnimator, 0.25, 0.3, true, false)
end
function CameraManager:SwitchToFlyThrough()
transitionToCameraAnimator(flythroughAnimator, 0.25, 0.3, true, false)
flythroughAnimator:Start()
end
function CameraManager:SwitchToAvatarEditor()
flythroughAnimator:Stop()
ZoneManager:SetZone("AvatarEditor")
avatarEditorCameraAnimator:Reset()
transitionToCameraAnimator(avatarEditorCameraAnimator, 0.25, 0.02, false, true)
end
local targetCFrame = avatarEditorZoomCFrames[2]
function CameraManager:UpdateAvatarEditorCamera(newCFrame)
targetCFrame = newCFrame
local frameInfo = avatarEditorCameraAnimator:get_getFrameInfo()()
avatarEditorCameraAnimator:Update(
{
frameInfo.CFrame,
targetCFrame
},
0.5
)
end
return CameraManager
@@ -0,0 +1,122 @@
-- Clean up this file when remove FIntXboxAvatarEditorRolloutPercent2
-- Written by Tomarty, Copyright Roblox 2015
local runService = game:GetService("RunService")
local ZoneAnimator;
do
local animators = {}
local function GetAnimator(zone)
if animators[zone] then
return animators[zone]
end
local moduleScript = script.Parent:WaitForChild("CameraManager_Zones"):FindFirstChild("CameraManagerZone_" .. tostring(zone))
if not moduleScript then
return
end
local animator = require(moduleScript)
animators[zone] = animator
return animator
end
ZoneAnimator = {
CurrentZone = nil;
}
local connection;
function ZoneAnimator:SetZone(zone)
if ZoneAnimator.CurrentZone == zone then
return
end
ZoneAnimator.CurrentZone = zone
if connection then
connection:disconnect()
connection = nil
end
local animator = GetAnimator(zone)
if not animator then
return
end
animator:SetEnabled(true)
connection = {
disconnect = function()
animator:SetEnabled(false)
end
}
end
end
local SkyboxManager;
do
SkyboxManager = {}
local activeSkybox = nil
function SkyboxManager:SetZone(id)
if activeSkybox then
activeSkybox.Parent = nil
activeSkybox = nil
end
local Skyboxes = game:GetService("ReplicatedStorage"):WaitForChild("Skyboxes")
local skybox = Skyboxes:FindFirstChild(id or "default")
if skybox then
activeSkybox = skybox:Clone()
local lighting = game:GetService("Lighting")
skybox.Parent = lighting
pcall(function()
for k, v in pairs(skybox:GetChildren()) do
lighting[v.Name] = v.Value
end
end)
end
end
end
local ZoneManager = {
Zone = nil;
}
--local ZoneAnimator = require(script.ZoneAnimator)
--local Skybox = require(script.Skybox)
local function setZoneInternal(zone)
ZoneManager.Zone = zone
SkyboxManager:SetZone(zone)
ZoneAnimator:SetZone(zone)
end
function ZoneManager:SetZone(zone)
if zone == ZoneManager.Zone then
return
end
if runService:IsRunning() then
setZoneInternal(zone)
else
spawn(function()
while not runService:IsRunning() do wait() end
setZoneInternal(zone)
end)
end
end
return ZoneManager
@@ -0,0 +1,218 @@
local runService = game:GetService("RunService")
local Zones = workspace:FindFirstChild("Zones")
local City = Zones and Zones:FindFirstChild("City")
local CFramer = {}; do
function CFramer.GetAllParts(model, list)
list = list or {}
if model:IsA("BasePart") then
list[#list + 1] = model
end
local children = model:GetChildren()
for i = 1, #children do
CFramer.GetAllParts(children[i], list)
end
return list
end
function CFramer.GetAllPartOffsets(parts, cframeBase)
-- The offset is a part's cframe relative to cframeBase
local offsets = {}
local cframeBase_inv = cframeBase:inverse()
for i = 1, #parts do
offsets[i] = cframeBase_inv * parts[i].CFrame
end
return offsets
end
function CFramer.newCFramer(parts, offsets)
return function(cf)
for i = 1, #parts do
parts[i].CFrame = cf * offsets[i]
end
end
end
function CFramer.newCFramerFromModel(model, cframeBase)
local parts = CFramer.GetAllParts(model)
local offsets = CFramer.GetAllPartOffsets(parts, cframeBase)
return CFramer.newCFramer(parts, offsets)
end
end
local renderTrains;
spawn(function()
while not runService:IsRunning() do wait(0.1) end
if not City then
City = workspace:WaitForChild("Zones"):WaitForChild("City")
end
local exampleTrain = City.Train:Clone()
local function newTrain()
local train = exampleTrain:Clone()
train.Parent = workspace
return CFramer.newCFramerFromModel(train.Parts, train.Track.CFrame)
end
local function newTrack(part, direction)
if not direction then
direction = math.random(0, 1) * 2 - 1
end
local length = part.Size.Y
local cf = part.CFrame * CFrame.Angles(math.pi / 2 * (direction + 1), 0, 0)
local cf0 = cf * CFrame.new(0, -length/2, 0)
return function(train, time)
local distance = time * 48
if distance >= 0 and distance <= length then
train(cf0 * CFrame.new(0, distance, 0))
end
end, part.Size.Y
end
local train0 = newTrain()
local train1 = newTrain()
local train2 = newTrain()
local train3 = newTrain()
local train4 = newTrain()
local train5 = newTrain()
local train6 = newTrain()
local tracks = City.Tracks
local track0 = newTrack(tracks.TrackA1)--, 1)
local track1 = newTrack(tracks.TrackA2)--, -1)
local track2 = newTrack(tracks.TrackA3)--, 1)
local track3 = newTrack(tracks.TrackB1)--, 1)
local track4 = newTrack(tracks.TrackB2)--, -1)
local track5 = newTrack(tracks.TrackC1)--, -1)
local track6 = newTrack(tracks.TrackC2)--, 1)
local track7 = newTrack(tracks.TrackC3)--, 1)
function renderTrains(timestamp)
track0(train0, timestamp % 15)
track1(train1, (timestamp - 3) % 20)
track2(train2, (timestamp - 5.5) % 18)
track3(train3, (timestamp - 5.5) % 25)
track6(train4, (timestamp - 2.5) % 30)
end
end)
local renderFlows;
spawn(function()
if not City then
City = workspace:WaitForChild("Zones"):WaitForChild("City")
end
local function newFlow(model, flowLength)
local parts = {}
local partParts = {}
local partPartOffsets = {}
local cframes = {}
for j, part in pairs(model:GetChildren()) do
local i = tonumber(part.Name)
parts[i] = part
cframes[i] = part.CFrame
local pParts = {}
local pPartOffsets = {}
for i, pPart in pairs(part:GetChildren()) do
if pPart:IsA("BasePart") then
pParts[#pParts + 1] = pPart
pPartOffsets[#pPartOffsets + 1] = part.CFrame:inverse() * pPart.CFrame
end
end
partParts[i] = pParts
partPartOffsets[i] = pPartOffsets
end
local partCycle = 0
local unitOffset = 0
local timestamp0 = 0
return function(timestamp1)
if timestamp1 < timestamp0 then
timestamp0 = timestamp1
end
local unit = (timestamp1 - timestamp0) / flowLength + unitOffset
if unit >= 1 then
partCycle = (partCycle + math.floor(unit)) % #parts
unit = unit % 1
unitOffset = unit
timestamp0 = timestamp1 -- - deltaTime * flowLength
end
for x = 0, #parts - 2 do
local i = (x - partCycle) % #cframes + 1
local cf = cframes[x + 1]:lerp(cframes[x + 2], unit)
parts[i].CFrame = cf
local pParts = partParts[i]
local pPartOffsets = partPartOffsets[i]
for n = 1, #pParts do
pParts[n].CFrame = cf * pPartOffsets[n]
end
end
end
end
local flow1 = newFlow(City.Pipe1.Flow, 0.5)
local flow2 = newFlow(City.Pipe2.Flow, 0.65)
function renderFlows(t)
flow1(t)
flow2(t)
end
end)
local connection;
local self = {}
function self:SetEnabled(enabled)
if enabled and connection then
return
end
if connection then
connection:disconnect()
connection = nil
end
if not enabled then
return
end
local timestamp0 = tick()
connection = game:GetService("RunService").RenderStepped:connect(function()
local t = tick() - timestamp0
if renderFlows then
renderFlows(t)
end
if renderTrains then
renderTrains(t)
end
end)
end
return self
@@ -0,0 +1,145 @@
local runService = game:GetService("RunService")
local CFrameOffsetter = {};
do
function CFrameOffsetter.GetAllParts(model, list)
list = list or {}
if model:IsA("BasePart") then
list[#list + 1] = model
end
local children = model:GetChildren()
for i = 1, #children do
CFrameOffsetter.GetAllParts(children[i], list)
end
return list
end
function CFrameOffsetter.GetAllPartOffsets(parts, cframeBase)
-- The offset is a part's cframe relative to cframeBase
local offsets = {}
local cframeBase_inv = cframeBase:inverse()
for i = 1, #parts do
offsets[i] = cframeBase_inv * parts[i].CFrame
end
return offsets
end
function CFrameOffsetter.newOffsetter(parts, offsets, cframeBase)
return function(offset)
local cf = cframeBase * offset
for i = 1, #parts do
parts[i].CFrame = cf * offsets[i]
end
end
end
function CFrameOffsetter.newOffsetterFromModel(model, cframeBase)
local parts = CFrameOffsetter.GetAllParts(model)
local offsets = CFrameOffsetter.GetAllPartOffsets(parts, cframeBase)
return CFrameOffsetter.newOffsetter(parts, offsets, cframeBase)
end
end
local newOffsetterFromModel = CFrameOffsetter.newOffsetterFromModel
local renderStation;
spawn(function()
while not runService:IsRunning() do wait(0.1) end
local Space = workspace:WaitForChild("Zones"):WaitForChild("Space")
local model = Space.Station
local cframeBase = model.rings.center.CFrame
local ring1 = newOffsetterFromModel(model.rings.Ring1, cframeBase)
local ring2 = newOffsetterFromModel(model.rings.Ring2, cframeBase)
local ring3 = newOffsetterFromModel(model.rings.Ring3, cframeBase)
local station = newOffsetterFromModel(model.station, cframeBase)
function renderStation(t)
ring1(
CFrame.Angles(0, (t / 6 % (math.pi * 2)), 0)
* CFrame.new(0, math.sin(t / 4 % (math.pi * 2)) * 1, 0)
)
ring2(
CFrame.Angles(0, -(t / 4 % (math.pi * 2)), 0)
* CFrame.new(0, math.sin(2 + t / 2 % (math.pi * 2)) * 1, 0)
)
ring3(
CFrame.Angles(0, (t / 3.5 % (math.pi * 2)), 0)
* CFrame.new(0, math.sin(1 + t / 2 % (math.pi * 2)) * 1, 0)
)
station(
CFrame.Angles(0, (t / -32 % (math.pi * 2)), 0)
* CFrame.new(0, math.sin(t / 4 % (math.pi * 2)) * 3 - 2, 0)
)
end
end)
local renderOrbit;
spawn(function()
while not runService:IsRunning() do wait(0.1) end
local Space = workspace:WaitForChild("Zones"):WaitForChild("Space")
local model = Space.Orbit
local cframeBase = model.center.CFrame
local cuteMoonThings = newOffsetterFromModel(model, cframeBase)
local ringParts = {Space.Disk, Space.Disk2, Space.Disk3}
local rings = {}
for i = 1, #ringParts do
rings[i] = newOffsetterFromModel(ringParts[i], ringParts[i].CFrame)
end
function renderOrbit(t)
cuteMoonThings(
CFrame.Angles((t / 80 % (math.pi * 2)), (t / 60 % (math.pi * 2)), (t / 40 % (math.pi * 2)))
)
for i = 1, #rings do
rings[i](
CFrame.Angles(0, 1 / 4 + (t / 60 % (math.pi * 2)), 0)
)
end
end
end)
local connection;
local self = {}
function self:SetEnabled(enabled)
if enabled and connection then
return
end
if connection then
connection:disconnect()
connection = nil
end
if not enabled then
return
end
local timestamp0 = tick()
connection = game:GetService("RunService").RenderStepped:connect(function()
local t = (tick() - timestamp0)
if renderStation then
renderStation(t)
end
if renderOrbit then
renderOrbit(t)
end
end)
end
return self
@@ -0,0 +1,70 @@
local runService = game:GetService("RunService")
local Volcano;
local lightning;
spawn(function()
while not runService:IsRunning() do wait(0.1) end
Volcano = workspace:WaitForChild("Zones"):WaitForChild("Volcano")
lightning = Volcano.Lightning:GetChildren()
for i = 1, #lightning do
lightning[i].Parent = nil
end
end)
local function playLighting(model)
if not (model and Volcano) then
return
end
model.Parent = Volcano.Lightning
wait(0.125)
model.Parent = nil
end
local connection;
local handle;
local self = {}
function self:SetEnabled(enabled)
if enabled and connection then
return
end
if connection then
connection:disconnect()
connection = nil
end
handle = nil
if not enabled then
return
end
local h = {} -- our handle
handle = h -- Stops a previous loop
coroutine.wrap(function()
while handle == h do
wait(math.random() * 16 + 1)
if lightning then
playLighting(lightning[math.random(1, #lightning)])
end
end
end)()
--[[
local timestamp0 = tick()
connection = game:GetService("RunService").RenderStepped:connect(function()
local t = (tick() - timestamp0)
--renderStation(t)
--renderOrbit(t)
end)
--]]
end
return self
@@ -0,0 +1,100 @@
local runService = game:GetService("RunService")
local ZoneAnimator;
do
local animators = {}
local function GetAnimator(zone)
if animators[zone] then
return animators[zone]
end
local moduleScript = script.Parent:WaitForChild("CameraManager_Zones"):FindFirstChild("CameraManagerZone_" .. tostring(zone))
if not moduleScript then
return
end
local animator = require(moduleScript)
animators[zone] = animator
return animator
end
ZoneAnimator = {
CurrentZone = nil;
}
local connection;
function ZoneAnimator:SetZone(zone)
if ZoneAnimator.CurrentZone == zone then
return
end
ZoneAnimator.CurrentZone = zone
if connection then
connection:disconnect()
connection = nil
end
local animator = GetAnimator(zone)
if not animator then
return
end
animator:SetEnabled(true)
connection = {
disconnect = function()
animator:SetEnabled(false)
end
}
end
end
local SkyboxManager;
do
SkyboxManager = {}
local activeSkybox = nil
function SkyboxManager:SetZone(id)
if activeSkybox then
activeSkybox.Parent = nil
activeSkybox:Destroy()
activeSkybox = nil
end
local Skyboxes = game:GetService("ReplicatedStorage"):WaitForChild("Skyboxes")
local skybox = Skyboxes:FindFirstChild(id or "default")
if skybox then
activeSkybox = skybox:Clone()
local lighting = game:GetService("Lighting")
activeSkybox.Parent = lighting
pcall(function()
for k, v in pairs(activeSkybox:GetChildren()) do
lighting[v.Name] = v.Value
end
end)
end
end
end
local ZoneManager = {
Zone = nil;
}
local function setZoneInternal(zone)
ZoneManager.Zone = zone
SkyboxManager:SetZone(zone)
ZoneAnimator:SetZone(zone)
end
function ZoneManager:SetZone(zone)
if zone == ZoneManager.Zone then
return
end
if runService:IsRunning() then
setZoneInternal(zone)
else
spawn(function()
while not runService:IsRunning() do wait() end
setZoneInternal(zone)
end)
end
end
return ZoneManager
@@ -0,0 +1,710 @@
--[[
// CarouselController.lua
// Controls how the data is updated for a carousel view
]]
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Modules = RobloxGui.Modules
local ContextActionService = game:GetService('ContextActionService')
local GuiService = game:GetService('GuiService')
local EventHub = require(Modules.Shell.EventHub)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local SoundManager = require(Modules.Shell.SoundManager)
local Utility = require(Modules.Shell.Utility)
local GameData = require(Modules.Shell.GameData)
local Http = require(Modules.Shell.Http)
local Strings = require(Modules.Shell.LocalizedStrings)
local BatchThumbnailLoader = require(Modules.Shell.BatchThumbnailLoader)
local ThumbnailLoader = require(Modules.Shell.ThumbnailLoader)
local FFlagXboxSponsoredGames = settings():GetFFlag("XboxSponsoredGames")
local function createCarouselController(view, disableBumperBinds, FrontPageIndex, EndPageIndex, AbsoluteDataIndex)
disableBumperBinds = disableBumperBinds or false
local this = {}
local PAGE_SIZE = 25 -- Can only be 10, 25, 50, 100
local MAX_PAGES = 2
local sortCollection = nil
local currentFocusData = nil
local absoluteDataIndex = tonumber(AbsoluteDataIndex) or 1
local guiServiceChangedCn = nil
local loadBuffer = 10
local pages = {}
local pageViews = {}
local isLoading = false
local frontPageIndex = tonumber(FrontPageIndex) or 1
local endPageIndex = tonumber(EndPageIndex) or 0
local isCuratedSort = false
local reachFront = true --For all sorts to mark if there's no previous page
local reachEnd = false --For all sorts to mark if there's no next page
-- Events
this.NewItemSelected = Utility.Signal()
local function getNewItem(itemData)
local placeId
if FFlagXboxSponsoredGames then
placeId = itemData.placeId
else
placeId = itemData
end
local data = GameData:GetGameData(placeId)
if data then
local item = Utility.Create"ImageButton"
{
Name = "CarouselViewImage",
BackgroundColor3 = GlobalSettings.ModalBackgroundColor,
BorderSizePixel = 0,
SoundManager:CreateSound("MoveSelection"),
}
local overrideSelection = view:GetSelectionImageObject()
if overrideSelection then
item.SelectionImageObject = overrideSelection
end
local thumbnailLoader
if data.ImageToken then
thumbnailLoader = BatchThumbnailLoader
BatchThumbnailLoader:CreateThumbnail(item, data.ImageToken, BatchThumbnailLoader.Sizes.Medium, true, false, nil)
else
thumbnailLoader = ThumbnailLoader:Create(item, data.IconId, ThumbnailLoader.Sizes.Medium, ThumbnailLoader.AssetType.SquareIcon)
spawn(function()
thumbnailLoader:LoadAsync(true, false, nil)
end)
end
local itemInfo = {
item = item,
thumbnailLoader = thumbnailLoader,
}
itemInfo.PlaceId = placeId
local transparencyTweens = {}
function itemInfo:AnimateTransparency(initial, final, duration)
Utility.CancelTweens(transparencyTweens)
local finished = nil
if initial < final then
thumbnailLoader:SetTransparency(final, item)
else
finished = function()
thumbnailLoader:SetTransparency(final, item)
end
end
table.insert(transparencyTweens,
Utility.PropertyTweener(
item,
"BackgroundTransparency",
initial,
final,
duration,
Utility.EaseOutQuad,
true))
table.insert(transparencyTweens,
Utility.PropertyTweener(
item,
"ImageTransparency",
initial,
final,
duration,
Utility.EaseOutQuad,
true,
finished))
end
return itemInfo
end
end
-- TODO: Remove this when caching is finished. Doing it this way is going to leave the old
-- data issues in place with the carousel
local function setInternalData(page)
if not page then
return {}
end
local newData = {}
newData.PreviousUrl = page.PreviousUrl
newData.NextUrl = page.NextUrl
local placeIds = page:GetPagePlaceIds()
local names = page:GetPagePlaceNames()
local voteData = page:GetPageVoteData()
local iconIds = page:GetPageIconIds()
local creatorNames = page:GetCreatorNames()
local creatorUserIds = page:GetPageCreatorUserIds()
for i = 1, #page.Data do
local gameEntry = {
Name = names[i];
PlaceId = placeIds[i];
IconId = iconIds[i];
VoteData = voteData[i];
CreatorName = creatorNames[i];
CreatorUserId = creatorUserIds[i];
-- Description and IsFavorites needs to be queried for each game when needed
Description = nil;
IsFavorited = nil;
GameData = nil;
}
table.insert(newData, gameEntry)
end
return newData
end
local onViewFocusChanged;
local function createPageView(page)
local pageView = {}
local viewItems = {}
if FFlagXboxSponsoredGames then
for i = 1, #page do
local itemData = page[i]
local placeId = itemData.placeId
local viewItemInfo = getNewItem(itemData)
if viewItemInfo then
viewItemInfo.item.Activated:connect(function()
if itemData.isSponsored then
spawn(function()
Http.PostSponsoredGameClickAsync(itemData.nativeAdData)
end)
end
if placeId then
EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId)
end
end)
table.insert(viewItems, viewItemInfo)
end
end
else
for i = 1, #page do
local itemData = page[i]
local placeId = itemData
local viewItemInfo = getNewItem(itemData)
if viewItemInfo then
viewItemInfo.item.Activated:connect(function()
if placeId then
EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId)
end
end)
table.insert(viewItems, viewItemInfo)
end
end
end
function pageView:LockFirstItem()
if viewItems and viewItems[1] and viewItems[1].item then
viewItems[1].item.NextSelectionLeft = viewItems[1].item
end
end
function pageView:UnlockFirstItem()
if viewItems and viewItems[1] and viewItems[1].item then
viewItems[1].item.NextSelectionLeft = nil
end
end
function pageView:LockLastItem()
if viewItems and viewItems[#viewItems] and viewItems[#viewItems].item then
viewItems[#viewItems].item.NextSelectionRight = viewItems[#viewItems].item
end
end
function pageView:UnlockLastItem()
if viewItems and viewItems[#viewItems] and viewItems[#viewItems].item then
viewItems[#viewItems].item.NextSelectionRight = nil
end
end
function pageView:GetCount()
return #viewItems
end
function pageView:GetItems()
return viewItems
end
function pageView:Destroy()
for i,itemInfo in pairs(viewItems) do
viewItems[i] = nil
itemInfo.item:Destroy()
end
end
return pageView
end
local function getPagesRangeIndex()
local lowIndex, highIndex
for index, page in pairs(pages) do
if not lowIndex or lowIndex > index then
lowIndex = index
end
if not highIndex or highIndex < index then
highIndex = index
end
end
return lowIndex, highIndex
end
-- For all sorts except curated sort
local function getPageAsync(pageIndex)
if pageIndex < 1 then
return nil
end
--Caching is done inside sortsdata
local startIndex = (pageIndex - 1) * PAGE_SIZE
local newPageData = sortCollection:GetSortAsync(startIndex, PAGE_SIZE)
if not newPageData then
return nil
end
if #newPageData > 0 then
pages[pageIndex] = newPageData
end
return pages[pageIndex]
end
-- For curated sort
local function getNextPageAsync(pageIndex)
--Caching is done inside sortsdata
local newPageData = sortCollection:GetCuratedSortAsync(pageIndex, PAGE_SIZE)
-- No data in current page
if not newPageData or #newPageData == 0 then
reachEnd = true
return nil
end
-- Current page is last page
if #newPageData < PAGE_SIZE then
reachEnd = true
end
pages[pageIndex] = newPageData
return pages[pageIndex]
end
-- For curated sort
local function getPreviousPageAsync(pageIndex)
-- pageIndex should be >= 0
-- When pageIndex == 0, means hasPrevPage is true in page1. Then we should fetch page0, insert front, and move all page index +1
if pageIndex < 0 then
reachFront = true
return nil
end
--Caching is done inside sortsdata
local newPageData, hasPrevPage = sortCollection:GetCuratedSortAsync(pageIndex, PAGE_SIZE, true)
-- No data in current page
if not newPageData or #newPageData == 0 then
reachFront = true
return nil
end
-- Current page is first page
if not hasPrevPage or #newPageData < PAGE_SIZE then
reachFront = true
end
if pageIndex == 0 then
--Move all pages to their next pages
local lowIndex, highIndex = getPagesRangeIndex()
for i = highIndex, 1, -1 do
pages[i + 1] = pages[i]
end
pages[1] = newPageData
else
pages[pageIndex] = newPageData
end
-- Increase 1 for all page indexes if we insert page at front
if pageIndex == 0 then
pageIndex = 1
frontPageIndex = frontPageIndex + 1
endPageIndex = endPageIndex + 1
end
return pages[pageIndex]
end
-- For curated sort. If getPreviousPageAsync() reach front and frontPageIndex > 1, reset page index
local function resetPageIndex()
local pageOffset = frontPageIndex - 1
frontPageIndex = 1
endPageIndex = endPageIndex - pageOffset
-- Remove previous pageviews
for i = 1, pageOffset do
local frontPageView = pageViews[i]
if frontPageView then
pageViews[i] = nil
view:RemoveAmountFromFront(frontPageView:GetCount())
frontPageView:Destroy()
end
end
-- Move pages and pageViews to left
for i = 1, endPageIndex do
pages[i] = pages[i + pageOffset]
pageViews[i] = pageViews[i + pageOffset]
end
for i = endPageIndex + 1, endPageIndex + pageOffset do
pages[i] = nil
pageViews[i] = nil
end
absoluteDataIndex = absoluteDataIndex - (pageOffset * PAGE_SIZE)
end
local previousFocusItem = nil
function onViewFocusChanged(newFocusItem)
local offset = 0
if previousFocusItem then
offset = view:GetItemIndex(newFocusItem) - view:GetItemIndex(previousFocusItem)
end
local visibleItemCount = view:GetVisibleCount()
local itemCount = view:GetCount()
if offset > 0 then
-- scrolled right
--lastVisibleItemIndex = firstVisibleItemIndex + visibleItemCount - 1
local firstVisibleItemIndex = view:GetFirstVisibleItemIndex()
local lastVisibleItemIndex = view:GetLastVisibleItemIndex()
-- Utility.DebugLog("Scroll right", frontPageIndex, endPageIndex, absoluteDataIndex + offset, offset, "needItemCount:", lastVisibleItemIndex + loadBuffer - itemCount, "reachEnd", reachEnd)
--Scrolling forward: If the last visible item(right most one) is no more than loadBuffer's steps from the trail, we will truncate new ones at back
if not isLoading and not reachEnd and lastVisibleItemIndex + loadBuffer >= itemCount then
isLoading = true
spawn(function()
local page
if isCuratedSort then
page = getNextPageAsync(endPageIndex + 1)
else
page = getPageAsync(endPageIndex + 1)
end
if page then
local newView = createPageView(page)
endPageIndex = endPageIndex + 1
view:InsertCollectionBack(newView:GetItems())
pageViews[endPageIndex] = newView
if pageViews[endPageIndex - 1] then
pageViews[endPageIndex - 1]:UnlockLastItem()
end
newView:LockLastItem()
if view:GetCount() > PAGE_SIZE * MAX_PAGES then
local frontPageView
frontPageView = pageViews[frontPageIndex]
--Don't erase the front page if the firstVisibleItem becomes too close to the leading edge
if firstVisibleItemIndex - frontPageView:GetCount() - loadBuffer > 0 then
pageViews[frontPageIndex] = nil
frontPageIndex = frontPageIndex + 1
view:RemoveAmountFromFront(frontPageView:GetCount())
frontPageView:Destroy()
if reachFront then
reachFront = false
end
end
end
else
reachEnd = true
end
isLoading = false
end)
end
elseif offset < 0 then
-- scrolled left
--firstVisibleItemIndex = lastVisibleItemIndex - visibleItemCount + 1
local firstVisibleItemIndex = view:GetFirstVisibleItemIndex()
local lastVisibleItemIndex = view:GetLastVisibleItemIndex()
-- Utility.DebugLog("Scroll left", frontPageIndex, endPageIndex, absoluteDataIndex + offset, offset, "firstVisibleItemIndex", firstVisibleItemIndex, "reachFront", reachFront)
--Scrolling backward: If the first visible item(left most one) is no more than loadBuffer's steps from the head, we will truncate new ones at front
if not isLoading and not reachFront and firstVisibleItemIndex - loadBuffer <= 0 then
isLoading = true
spawn(function()
local page
if isCuratedSort then
page = getPreviousPageAsync(frontPageIndex - 1)
else
page = getPageAsync(frontPageIndex - 1)
end
if page then
local newView = createPageView(page)
frontPageIndex = frontPageIndex - 1
view:InsertCollectionFront(newView:GetItems())
pageViews[frontPageIndex] = newView
if pageViews[frontPageIndex + 1] then
pageViews[frontPageIndex + 1]:UnlockFirstItem()
end
newView:LockFirstItem()
if view:GetCount() > PAGE_SIZE * MAX_PAGES then
local endPageView
endPageView = pageViews[endPageIndex]
--Don't erase the trail page if the lastVisibleItemIndex becomes too close to the trail
if lastVisibleItemIndex + loadBuffer < itemCount - endPageView:GetCount() then
pageViews[endPageIndex] = nil
endPageIndex = endPageIndex - 1
view:RemoveAmountFromBack(endPageView:GetCount())
endPageView:Destroy()
if reachEnd then
reachEnd = false
end
end
end
else
reachFront = true
end
if isCuratedSort and reachFront and frontPageIndex > 1 then
resetPageIndex()
end
isLoading = false
end)
end
end
absoluteDataIndex = absoluteDataIndex + offset
previousFocusItem = newFocusItem
view:ChangeFocus(newFocusItem)
local pageNumber = math.ceil(absoluteDataIndex/PAGE_SIZE)
local pageDataIndex = absoluteDataIndex - ((pageNumber - 1) * PAGE_SIZE)
currentFocusData = pages[pageNumber][pageDataIndex]
if FFlagXboxSponsoredGames then
this.NewItemSelected:fire(currentFocusData.placeId, nil, currentFocusData.isSponsored)
else
this.NewItemSelected:fire(currentFocusData)
end
end
--[[ Public API ]]--
function this:GetFrontGameData()
if pages[frontPageIndex] then
return pages[frontPageIndex][1]
end
end
function this:GetCurrentFocusGameData()
return currentFocusData
end
function this:SelectFront()
local frontViewItem = view:GetFront()
if frontViewItem then
onViewFocusChanged(frontViewItem)
end
end
function this:SetLoadBuffer(newValue)
loadBuffer = newValue
end
function this:InitializeAsync(gameCollection)
view:RemoveAllItems()
frontPageIndex = frontPageIndex or 1
endPageIndex = endPageIndex or 0
absoluteDataIndex = absoluteDataIndex or 1
currentFocusData = nil
pages = {}
pageViews = {}
sortCollection = gameCollection
isCuratedSort = gameCollection and gameCollection.GameSetTargetId and gameCollection.GameSetTargetId ~= 0
-- Utility.DebugLog("CarouselController:InitializeAsync", gameCollection.GameSetTargetId, isCuratedSort)
local loadPrevDataFail = true
-- For refreshed sorts, currenlty only RecentlyPlayed and Favorite sorts
if endPageIndex >= frontPageIndex and absoluteDataIndex > 0 then
local pageCount = 0
local maxDataIndex = (frontPageIndex - 1) * PAGE_SIZE
for i = frontPageIndex, endPageIndex do
local page = isCuratedSort and getNextPageAsync(i) or getPageAsync(i)
if page then
local newView = createPageView(page)
view:InsertCollectionBack(newView:GetItems())
pageViews[i] = newView
maxDataIndex = maxDataIndex + newView:GetCount()
pageCount = pageCount + 1
else
break
end
end
endPageIndex = frontPageIndex + pageCount - 1
if pageCount > 0 then
local viewItem
--if requested index overflow, we select the trail
if absoluteDataIndex > maxDataIndex then
absoluteDataIndex = maxDataIndex
viewItem = view:GetBack()
else
local viewIndex = absoluteDataIndex - (frontPageIndex - 1) * PAGE_SIZE
viewItem = view:GetItemAt(viewIndex)
end
if viewItem then
previousFocusItem = viewItem
--Use ChangeFocus to focus on the prev index immediately
view:ChangeFocus(viewItem, 0)
loadPrevDataFail = false
end
end
if loadPrevDataFail then --Clear data again
view:RemoveAllItems()
frontPageIndex = 1
endPageIndex = 0
absoluteDataIndex = 1
currentFocusData = nil
pages = {}
for i = 1, #pageViews do
pageViews[i]:Destroy()
pageViews[i] = nil
end
pageViews = {}
end
end
if loadPrevDataFail then
for i = 1, MAX_PAGES do
local page
if isCuratedSort then
page = getNextPageAsync(i)
else
page = getPageAsync(i)
end
if page then
local newView = createPageView(page)
endPageIndex = endPageIndex + 1
view:InsertCollectionBack(newView:GetItems())
pageViews[i] = newView
else
break
end
end
local frontViewItem = view:GetFront()
if frontViewItem then
absoluteDataIndex = 1
previousFocusItem = frontViewItem
view:SetFocus(frontViewItem)
end
end
if pageViews and endPageIndex > 0 then
if pageViews[frontPageIndex] then
pageViews[frontPageIndex]:LockFirstItem()
end
if pageViews[endPageIndex] then
pageViews[endPageIndex]:LockLastItem()
end
end
end
function this:GetIndexData()
return frontPageIndex, endPageIndex, absoluteDataIndex
end
function this:Connect()
guiServiceChangedCn = Utility.DisconnectEvent(guiServiceChangedCn)
guiServiceChangedCn = GuiService:GetPropertyChangedSignal('SelectedCoreObject'):connect(function()
local newSelection = GuiService.SelectedCoreObject
if newSelection and view:ContainsItem(newSelection) then
onViewFocusChanged(newSelection)
end
end)
local function getItemNextShiftItem(direction)
local currentFocusIndex = view:GetItemIndex(previousFocusItem)
local shiftAmount = view:GetFullVisibleItemCount()
local nextItem = view:GetItemAt(currentFocusIndex + shiftAmount * direction)
if not nextItem then
nextItem = direction == 1 and view:GetBack() or view:GetFront()
end
return nextItem
end
local function shiftRight()
local nextItem = getItemNextShiftItem(1)
if nextItem then
GuiService.SelectedCoreObject = nextItem
end
end
local function shiftLeft()
local nextItem = getItemNextShiftItem(-1)
if nextItem then
GuiService.SelectedCoreObject = nextItem
end
end
local seenRightBumper = false
local function onBumperRight(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenRightBumper = true
elseif seenRightBumper and inputState == Enum.UserInputState.End then
local currentSelection = GuiService.SelectedCoreObject
if currentSelection and view:ContainsItem(currentSelection) then
shiftRight()
end
seenRightBumper = false
end
end
local seenLeftBumper = false
local function onBumperLeft(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
seenLeftBumper = true
elseif seenLeftBumper and inputState == Enum.UserInputState.End then
local currentSelection = GuiService.SelectedCoreObject
if currentSelection and view:ContainsItem(currentSelection) then
shiftLeft()
end
seenRightBumper = false
end
end
-- Bumper Binds
if not disableBumperBinds then
ContextActionService:UnbindCoreAction("BumperRight")
ContextActionService:UnbindCoreAction("BumperLeft")
ContextActionService:BindCoreAction("BumperRight", onBumperRight, false, Enum.KeyCode.ButtonR1)
ContextActionService:BindCoreAction("BumperLeft", onBumperLeft, false, Enum.KeyCode.ButtonL1)
end
end
function this:Disconnect()
guiServiceChangedCn = Utility.DisconnectEvent(guiServiceChangedCn)
if not disableBumperBinds then
ContextActionService:UnbindCoreAction("BumperRight")
ContextActionService:UnbindCoreAction("BumperLeft")
end
end
function this:HasResults()
return endPageIndex - frontPageIndex >= 0
end
return this
end
return createCarouselController
@@ -0,0 +1,393 @@
--[[
// CarouselView.lua
// View for a carousel. Used for GameGenre screen
// TODO: Support Vertical?
//
// Current this supports a focus that is aligned to the left (0, 0), in the future we
// could do other alignments if we need them
]]
local CoreGui = game:GetService("CoreGui")
local GuiRoot = CoreGui:FindFirstChild("RobloxGui")
local Modules = GuiRoot:FindFirstChild("Modules")
local ShellModules = Modules:FindFirstChild("Shell")
local Utility = require(ShellModules:FindFirstChild('Utility'))
local GameData = require(ShellModules:FindFirstChild('GameData'))
local function createCarouselView()
local this = {}
local itemInfos = {}
local padding = 0
local itemSizePercentOfContainer = 1
local focusItem = nil
local BASE_TWEEN_TIME = 0.2
local isViewFocused = false
local overrideSelectionImage = nil
local incomingSelectableValue = true;
local incomingImageColor = Color3.new(1,1,1);
local DISABLED_COLOR = Color3.new(0.4, 0.4, 0.4)
local ENABLED_COLOR = Color3.new(1, 1, 1)
local container = Utility.Create'ScrollingFrame'
{
Name = "CarouselContainer";
BackgroundTransparency = 1;
ClipsDescendants = false;
ScrollingEnabled = false;
Selectable = false;
ScrollBarThickness = 0;
}
local function isVisible(item)
return item.AbsolutePosition.x + item.AbsoluteSize.x >= 0 and
item.AbsolutePosition.x < GuiRoot.AbsoluteSize.x
end
local function getFocusSize()
local scaling = itemSizePercentOfContainer
if isViewFocused then
scaling = 1
end
local size = container.Size.Y.Offset * scaling
return UDim2.new(0, size, 0, size)
end
local function getNonFocusSize()
local size = container.Size.Y.Offset * itemSizePercentOfContainer
return UDim2.new(0, size, 0, size)
end
local function getItemSize(item)
if item == focusItem then
return getFocusSize()
else
return getNonFocusSize()
end
end
local function getItemLayoutPosition(index)
local focusIndex = this:GetItemIndex(focusItem)
local offsetFromFocus = index - focusIndex
local x, y = 0, 0
if index > focusIndex then
-- items to the right of focus need additional buffer due to focus size being larger
x = getFocusSize().X.Offset + offsetFromFocus * padding + (offsetFromFocus - 1) * getNonFocusSize().X.Offset
else
x = offsetFromFocus * padding + offsetFromFocus * getNonFocusSize().X.Offset
end
local size = (focusIndex == index) and getFocusSize() or getNonFocusSize()
y = (container.Size.Y.Offset - size.Y.Offset) / 2
return UDim2.new(0, x, 0, y)
end
local function recalcLayout(duration)
duration = duration or 0
for i = 1, #itemInfos do
local item = itemInfos[i].item
local size = getItemSize(item)
local position = getItemLayoutPosition(i)
if item:IsDescendantOf(game) then
item:TweenSizeAndPosition(size, position, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, duration, true)
else
item.Size = size
item.Position = position
end
end
end
--[[ Public API ]]--
function this:ChangeFocus(newFocus, tweenTime)
-- We don't use SetFocus() as that function will do a recalc. We want to get the next position
-- from current position, not recalcd postion for each item
if self:ContainsItem(newFocus) then
focusItem = newFocus
recalcLayout(tweenTime or BASE_TWEEN_TIME)
end
end
function this:SetSize(newSize)
if newSize ~= container.Size then
container.Size = newSize
container.CanvasSize = UDim2.new(0, container.Size.X.Offset * 2, 1, 0)
recalcLayout()
end
end
function this:SetPosition(newPosition)
container.Position = newPosition
end
function this:SetPadding(newPadding)
if newPadding ~= padding then
padding = newPadding
recalcLayout()
end
end
function this:SetItemSizePercentOfContainer(value)
if value ~= itemSizePercentOfContainer then
itemSizePercentOfContainer = value
recalcLayout()
end
end
function this:SetParent(newParent)
container.Parent = newParent
end
function this:SetFocus(newFocusItem)
if self:ContainsItem(newFocusItem) and newFocusItem ~= focusItem then
focusItem = newFocusItem
recalcLayout()
end
end
function this:SetClipsDescendants(value)
container.ClipsDescendants = value
end
local DEFAULT_FADE_DURATION = 0.2
local targetTransparency = 0
function this:SetTransparency(value, duration, refresh)
if not refresh and value == targetTransparency then return end
if duration then
targetTransparency = Utility.Clamp(0, 1, targetTransparency)
if not refresh and value == targetTransparency then return end
else
duration = DEFAULT_FADE_DURATION
end
for _, itemInfo in pairs(itemInfos) do
itemInfo:AnimateTransparency(targetTransparency, value, duration)
end
targetTransparency = value
end
function this:GetTransparency()
return targetTransparency
end
function this:SetSelectable(value)
incomingImageColor = ENABLED_COLOR
if not value then
incomingImageColor = DISABLED_COLOR
end
incomingSelectableValue = value
for _, itemInfo in pairs(itemInfos) do
itemInfo.item.Selectable = incomingSelectableValue
itemInfo.item.ImageColor3 = incomingImageColor
end
end
function this:GetAvailableItem()
if focusItem and focusItem.Parent then
return focusItem
end
return self:GetFront()
end
function this:GetFocusItem()
return focusItem
end
function this:GetItemAt(index)
return itemInfos[index] and itemInfos[index].item
end
function this:GetFront()
return itemInfos[1] and itemInfos[1].item
end
function this:GetBack()
return itemInfos[#itemInfos] and itemInfos[#itemInfos].item
end
function this:GetItemIndex(item)
for i = 1, #itemInfos do
if itemInfos[i].item == item then
return i
end
end
return 0
end
function this:GetCount()
return #itemInfos
end
function this:GetVisibleCount()
local visibleItemCount = 0
for i = 1, #itemInfos do
if isVisible(itemInfos[i].item) then
visibleItemCount = visibleItemCount + 1
end
end
return visibleItemCount
end
function this:GetFirstVisibleItemIndex()
for i = 1, #itemInfos do
if isVisible(itemInfos[i].item) then
return i
end
end
end
function this:GetLastVisibleItemIndex()
for i = #itemInfos, 1, -1 do
if isVisible(itemInfos[i].item) then
return i
end
end
end
function this:GetFullVisibleItemCount()
local containerSizeX = container.AbsoluteSize.x
-- remove focus from the size, and figure out how many other items can fit
local fittingSize = containerSizeX - getFocusSize().X.Offset
if fittingSize <= 0 then
return 0
end
local itemSize = getNonFocusSize().X.Offset + padding
local count = math.floor(fittingSize/itemSize) + 1
return count
end
function this:InsertCollectionFront(collection)
for i = #collection, 1, -1 do
local itemInfo = collection[i]
local item = itemInfo.item
item.Selectable = incomingSelectableValue
item.ImageColor3 = incomingImageColor
item.ImageTransparency = targetTransparency
item.BackgroundTransparency = targetTransparency
-- set item position in front of front item
item.Position = getItemLayoutPosition(0)
item.Size = getItemSize(item)
table.insert(itemInfos, 1, itemInfo)
item.Parent = container
GameData:AddRelatedGuiObject(itemInfo.PlaceId, item)
end
end
function this:InsertCollectionBack(collection)
for i = 1, #collection do
local itemInfo = collection[i]
local item = itemInfo.item
item.Selectable = incomingSelectableValue
item.ImageColor3 = incomingImageColor
item.ImageTransparency = targetTransparency
item.BackgroundTransparency = targetTransparency
-- set item position behind last item
item.Position = getItemLayoutPosition(#itemInfos + 1)
item.Size = getItemSize(item)
table.insert(itemInfos, itemInfo)
item.Parent = container
GameData:AddRelatedGuiObject(itemInfo.PlaceId, item)
end
end
function this:RemoveAmountFromFront(amount)
for i = 1, amount do
local item = table.remove(itemInfos, 1).item
item.Parent = nil
end
recalcLayout()
end
function this:RemoveAmountFromBack(amount)
for i = 1, amount do
local item = table.remove(itemInfos).item
item.Parent = nil
end
recalcLayout()
end
function this:RemoveItem(item)
for i = 1, #itemInfos do
if itemInfos[i].item == item then
local removedItem = table.remove(itemInfos, i).item
removedItem.Parent = nil
break
end
end
recalcLayout()
end
function this:RemoveAllItems()
for i = #itemInfos, 1, -1 do
local item = table.remove(itemInfos, #itemInfos).item
item.Parent = nil
end
end
function this:ContainsItem(item)
if not item then
return false
end
return item.Parent == container
end
local function onFocusChanged()
this:ChangeFocus(focusItem)
end
function this:Focus()
if isViewFocused then return end
isViewFocused = true
onFocusChanged()
end
function this:GetContainer()
return container
end
function this:RemoveFocus()
if not isViewFocused then return end
isViewFocused = false
onFocusChanged()
end
function this:SetSelectionImageObject(guiObject)
overrideSelectionImage = guiObject
end
function this:GetSelectionImageObject()
return overrideSelectionImage
end
return this
end
return createCarouselView
@@ -0,0 +1,78 @@
local TextService = game:GetService('TextService')
local ShellModules = script.Parent.Parent
local GlobalSettings = require(ShellModules.GlobalSettings)
local Strings = require(ShellModules.LocalizedStrings)
local Utility = require(ShellModules.Utility)
local AccountAgeStatus = {}
AccountAgeStatus.__index = AccountAgeStatus
function AccountAgeStatus.new(store, parent)
local self = {}
self.StoreChangedCn = nil
self.rbx = Utility.Create'Frame' {
Name = "AccountAgeContainer",
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
Parent = parent,
Utility.Create'TextLabel' {
Name = "AccountAgeText",
Size = UDim2.new(1, -12, 1, -12),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = Enum.FontSize.Size14,
TextColor3 = GlobalSettings.WhiteTextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
Text = "",
ZIndex = 2,
}
}
self.StoreChangedCn = store.changed:connect(function(newState, oldState)
if newState.RobloxUser.under13 ~= oldState.RobloxUser.under13 then
local isUnder13 = newState.RobloxUser.under13
-- clear out if under13 is nil
if isUnder13 == nil then
self.rbx.AccountAgeText.Text = ""
return
end
local newText = Strings:LocalizedString("AccountUnder13Phrase")
if isUnder13 == false then
newText = Strings:LocalizedString("AccountOver13Phrase")
end
if newText == self.rbx.AccountAgeText.Text then
return
end
self.rbx.AccountAgeText.Text = newText
-- update layout
local textSize = TextService:GetTextSize(newText,
Utility.ConvertFontSizeEnumToInt(self.rbx.AccountAgeText.FontSize), self.rbx.AccountAgeText.Font, Vector2.new(0, 0))
self.rbx.Size = UDim2.new(0, textSize.x, 0, 50)
self.rbx.Position = UDim2.new(1, -textSize.x, 0, 0)
end
end)
setmetatable(self, AccountAgeStatus)
return self
end
function AccountAgeStatus:Destruct()
self.rbx:Destroy()
self.rbx = nil
if self.StoreChangedCn then
self.StoreChangedCn:disconnect()
self.StoreChangedCn = nil
end
end
return AccountAgeStatus
@@ -0,0 +1,41 @@
return function()
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Store = require(Modules.Common.Rodux).Store
local AccountAgeStatus = require(script.Parent.AccountAgeStatus)
local SetRobloxUser = require(Modules.Shell.Actions.SetRobloxUser)
it("should construct and destroy the object", function()
local reducer = require(Modules.Shell.Reducers.AppShellReducer)
local store = Store.new(reducer, {})
local object = AccountAgeStatus.new(store, nil)
expect(object).to.be.ok()
expect(object).to.be.a("table")
object:Destruct()
expect(object.rbx).never.to.be.ok()
expect(object.StoreChangedCn).never.to.be.ok()
store:destruct()
end)
it("should update status on store changed", function()
local reducer = require(Modules.Shell.Reducers.AppShellReducer)
local store = Store.new(reducer, {})
local object = AccountAgeStatus.new(store, nil)
expect(object.rbx.AccountAgeText.Text).to.equal("")
local userInfo = {
under13 = true,
}
store:dispatch(SetRobloxUser(userInfo))
store:flush()
expect(object.rbx.AccountAgeText.Text).never.to.equal("")
store:destruct()
end)
end
@@ -0,0 +1,237 @@
--[[
A Roact alert overlay screen.
Props:
Key : Variant - The key of the overlay screen.
InFocus : bool - Is the component in focus.
Title : String - The title of the screen.
Description : String - The description of the screen.
ImageLabel : Roact.Component - the image to be displayed.
ButtonTextYes : String - The text on the yes button.
ButtonTextNo : String - The text on the no button.
CallbackYes : function() - The callback function for the yes button.
CallbackNo : function() - The callback function for the no button.
CallbackBack : function() - The callback function for when back is pressed.
DefaultButton : 0 or 1 - The default selection for the overlay screen.
0 for Yes
1 for No
]]
local CoreGui = game:GetService("CoreGui")
local RobloxGui = CoreGui.RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Utility = require(Modules.Shell.Utility)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local SoundManager = require(Modules.Shell.SoundManager)
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
local AlertOverlay = Roact.PureComponent:extend("AlertOverlay")
local TEXT_LEFT_EDGE = 776
local CONFRIM_KEY = "ComfirmButtonKey"
local CANCEL_KEY = "CancelButtonKey"
function AlertOverlay:init()
self.key = self.props.Key
self.guiObjs = {}
self.defaultItemKey = CANCEL_KEY
if self.props.DefaultButton == 0 then
self.defaultItemKey = CONFRIM_KEY
end
self.state =
{
currentItemKey = self.defaultItemKey
}
self.onSelectionGained = function(key)
self:setState(
{
currentItemKey = key
})
end
end
function AlertOverlay:willUpdate(nextProps, nextState)
self.defaultItemKey = nil
if self.props.InFocus == nextProps.InFocus then
return
end
if nextProps.InFocus then
self.defaultItemKey = nextState.currentItemKey
end
end
function AlertOverlay:render()
local confirmButton;
if not self.props.CallbackYes and self.defaultItemKey == CONFRIM_KEY then
self.defaultItemKey = CANCEL_KEY
elseif not self.props.CallbackNo and self.defaultItemKey == CANCEL_KEY then
self.defaultItemKey = CONFRIM_KEY
end
if self.props.CallbackYes then
local confirmButtonProps =
{
Position = UDim2.new(0, TEXT_LEFT_EDGE, 1, -166);
AnchorPoint = Vector2.new(0,0),
Size = UDim2.new(0, 320, 0, 66);
}
local confirmButtonTextProps =
{
Text = self.props.ButtonTextYes,
TextXAlignment = Enum.TextXAlignment.Center,
}
confirmButton = Roact.createElement(RoundedButton,
{
Button = confirmButtonProps,
Text = confirmButtonTextProps,
Focused = self.props.InFocus and self.state.currentItemKey == CONFRIM_KEY,
Selected = self.defaultItemKey == CONFRIM_KEY,
OnSelectionGained = function()
self.onSelectionGained(CONFRIM_KEY)
end,
OnActivated = function()
SoundManager:Play('ButtonPress')
self.props.CallbackYes()
end
})
end
local cancelButton;
if self.props.CallbackNo then
local cancelButtonProps =
{
Position = UDim2.new(0, 1106, 1 ,-166),
AnchorPoint = Vector2.new(0, 0),
Size = UDim2.new(0, 320, 0, 66),
}
local cancelButtonTextProps =
{
Text = self.props.ButtonTextNo,
TextXAlignment = Enum.TextXAlignment.Center,
}
cancelButton = Roact.createElement(RoundedButton,
{
Button = cancelButtonProps,
Text = cancelButtonTextProps,
Focused = self.props.InFocus and self.state.currentItemKey == CANCEL_KEY,
Selected = self.defaultItemKey == CANCEL_KEY,
OnSelectionGained = function()
self.onSelectionGained(CANCEL_KEY)
end,
OnActivated = function()
SoundManager:Play('ButtonPress')
self.props.CallbackNo()
end
})
elseif self.defaultItemKey == CANCEL_KEY then
self.defaultItemKey = CONFRIM_KEY
end
local titleText = Roact.createElement("TextLabel",
{
Size = UDim2.new(0, 0, 0, 0),
Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 136),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.HeaderSize,
TextColor3 = GlobalSettings.WhiteTextColor,
Text = self.props.Title,
TextXAlignment = Enum.TextXAlignment.Left,
})
local descriptionText = Roact.createElement("TextLabel",
{
Size = UDim2.new(0, 762, 0, 304),
Position = UDim2.new(0, TEXT_LEFT_EDGE, 0, 200),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
Font = GlobalSettings.LightFont,
FontSize = GlobalSettings.TitleSize,
TextColor3 = GlobalSettings.WhiteTextColor,
TextWrapped = true,
Text = self.props.Description,
})
local reportIcon = self.props.ImageLabel
if reportIcon == nil then
reportIcon = Roact.createElement("ImageLabel",
{
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = GlobalSettings.Images.LargeErrorIcon,
Size = UDim2.new(0, 321, 0, 264),
})
end
local imageContainer = Roact.createElement("Frame",
{
Size = UDim2.new(0, 576, 0, 642),
Position = UDim2.new(0, 100, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
BorderSizePixel = 0,
BackgroundTransparency = 1,
},{
reportImage = reportIcon
})
local container = Roact.createElement("Frame",
{
Size = UDim2.new(1, 0, 0, 640),
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
BackgroundColor3 = GlobalSettings.Colors.OverlayColor;
},{
ImageContainer = imageContainer,
TitleText = titleText,
DescriptionText = descriptionText,
CancelButton = cancelButton,
ConfirmButton = confirmButton,
})
local modalOverlay = Roact.createElement("Frame",
{
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, 0, 0.5, 0),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.3,
[Roact.Ref] = function(rbx)
self.ref = rbx
end,
},{
Container = container,
})
local redirectObj;
if self.props.CallbackBack then
redirectObj = Roact.createElement(RedirectComponent,
{
Key = self.key,
InFocus = true,
RedirectBack = function()
self.props.CallbackBack()
end,
})
end
return Roact.createElement(Roact.Portal,
{
target = CoreGui
},{
[self.key] = Roact.createElement("ScreenGui",
{
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
DisplayOrder = 1
},
{
ModalOverlay = modalOverlay,
RedirectObj = redirectObj,
}),
})
end
function AlertOverlay:didMount()
delay(0,function()
Utility.AddSelectionParent(self.key, self.ref)
end)
SoundManager:Play("OverlayOpen")
end
function AlertOverlay:willUnmount()
Utility.RemoveSelectionGroup(self.key)
end
return AlertOverlay
@@ -0,0 +1,82 @@
--[[
A simple base screen for Roact components.
Props:
BackPageTitle : string - The title of the parent page
Content : Roact.Component - The content of the frame
]]
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local ContextActionEvent = require(Modules.Shell.Components.ContextActionEvent)
local BaseScreen = Roact.PureComponent:extend("BaseScreen")
local BACK_IMAGE = "rbxasset://textures/ui/Shell/Icons/BackIcon@1080.png"
local BACK_IMAGE_SIZE = 48
function BaseScreen:init()
self.onBack = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
self._seenPressed = true
elseif inputState == Enum.UserInputState.End and self._seenPressed then
self.props.onUnmount()
end
end
self.onCreate = function(rbx)
end
end
function BaseScreen:render()
local backPageTitle = self.props.BackPageTitle
local onCreate = self.props.OnCreate or self.onCreate
return Roact.createElement("Frame",
{
[Roact.Ref] = function(rbx)
onCreate(rbx)
end,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5,0,0.5,0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
},{
backImage = Roact.createElement("ImageButton",
{
Size = UDim2.new(0,BACK_IMAGE_SIZE,0,BACK_IMAGE_SIZE),
BackgroundTransparency = 1,
Image = BACK_IMAGE,
Selectable = false,
[Roact.Event.Activated] = self.onBack
}),
BackPageTitleLabel = Roact.createElement("TextLabel",
{
Size = UDim2.new(0, 0, 0, BACK_IMAGE_SIZE),
Position = UDim2.new(0,BACK_IMAGE_SIZE+8,0,BACK_IMAGE_SIZE/2),
AnchorPoint = Vector2.new(0,0.5),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.ButtonSize,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = GlobalSettings.WhiteTextColor,
Text = backPageTitle
}),
-- NOTE: This will need to be changed when the screen is actually connected to the tree.
BackConnector = Roact.createElement(ContextActionEvent, {
name = "GoBackTo" .. backPageTitle,
callback = self.onBack,
binds = { Enum.KeyCode.ButtonB },
}),
view = Roact.createElement("Frame",
{
Size = UDim2.new(1,0,1,-(BACK_IMAGE_SIZE+2)),
BackgroundTransparency = 1,
Position = UDim2.new(0,0,0,(BACK_IMAGE_SIZE+2)),
},{self.props.Content})
})
end
return BaseScreen
@@ -0,0 +1,156 @@
--[[
Creates a Roact component provides a vertical list menu for multiple elements
Props:
Key : Variant - The key for this category menu. The key cannot be changed after init.
InFocus : bool - Is the component in focus.
Navigator : Navigator - The Navigator object.
DefaultCategoryFocus : Variant - This is the key of the default focus button.
DefaultCategoryKey : Variant - The default selection of the category menu.
Categories : Categories - An object that provides data needed for the creating the categories
The object must have an order mapping and a StringKeys mapping for localization.
E.g.
Categories[CategoryKey].Key = "Category"
Categories[CategoryKey].Order = 1
Categories[CategoryKey].StringKey = "CategoryStringKey"
OnSelectSection : function(Key : Variant) - Callback for when a section of the key is select.
OnLeaveSection : function(Key : Variant) - Callback for when a section of the key is no longer selected.
EnterSection : function() - Callback functin to enter the menu section.
RedirectUp : function() - Callback functin when redirect up.
RedirectDown : function() - Callback functin when redirect down.
ActionPriority : int - The action priority.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Utility = require(Modules.Shell.Utility)
local LocalizedStrings = require(Modules.Shell.LocalizedStrings)
local VerticalListView = require(Modules.Shell.Components.Common.VerticalListView)
local SelectorButton = require(Modules.Shell.Components.Common.SelectorButton)
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
local CategoryMenuView = Roact.PureComponent:extend("CategoryMenuView")
local BUTTON_WIDTH = 360
local BUTTON_HEIGHT = 80
function CategoryMenuView:OnSelectSection(key)
if self.state.currentSectionKey ~= key then
self:setState(
{
currentSectionKey = key,
})
end
end
function CategoryMenuView:init()
self.key = self.props.Key
self.defaultCategoryKey = self.props.DefaultCategoryKey
self.defaultCategoryFocus = self.defaultCategoryKey or self.props.DefaultCategoryFocus
self.state =
{
currentSectionKey = self.props.DefaultCategoryFocus,
}
self.getCurrentPageIndex = function()
return self.CurrentSection
end
end
function CategoryMenuView:willUpdate(nextProps, nextState)
self.defaultCategoryKey = nil
if self.props.InFocus == nextProps.InFocus then
return
end
if nextProps.InFocus then
self.defaultCategoryKey = nextState.currentSectionKey or self.props.DefaultCategoryKey
end
end
function CategoryMenuView:render()
self.categories = self.props.Categories
local buttons = {}
for k in pairs(self.categories) do
buttons[k] = Roact.createElement(SelectorButton,
{
Size = UDim2.new(0, BUTTON_WIDTH, 0, BUTTON_HEIGHT),
AnchorPoint = Vector2.new(0, 0),
Text = LocalizedStrings:LocalizedString(self.categories[k].StringKey),
Key = k,
LayoutOrder = self.categories[k].Order,
Focused = self.state.currentSectionKey == k,
Selected = self.defaultCategoryKey == k,
OnSelectionGained = function(key)
self:OnSelectSection(key)
if self.props.OnSelectSection then
self.props.OnSelectSection(key)
end
end,
OnSelectionLost = self.props.OnLeaveSection,
OnActivated = self.props.EnterSection,
})
end
local navObj = Roact.createElement(RedirectComponent,
{
ActionPriority = self.props.ActionPriority,
Key = self.key,
InFocus = self.props.InFocus,
RedirectRight = self.props.EnterSection,
RedirectUp = self.props.RedirectUp,
RedirectDown = self.props.RedirectDown,
})
local buttonView = Roact.createElement(VerticalListView,
{
PaddingTop = UDim.new(0.005, 0),
PaddingBottom = UDim.new(0.005, 0),
Spacing = UDim.new(0.025, 0),
ScrollBarThickness = 0,
Items = buttons,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
ScrollingEnabled = false,
})
return Roact.createElement("Frame",
{
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0, 0),
Position = UDim2.new(0, 0, 0, 0),
BackgroundTransparency = 1,
Selectable = false,
[Roact.Ref] = function(rbx)
self.ref = rbx
end,
},{
NavObj = navObj,
ButtonView = buttonView,
})
end
function CategoryMenuView:didMount()
delay(0, function()
if self.props.InFocus and self.ref then
Utility.RemoveSelectionGroup(self.key)
Utility.AddSelectionParent(self.key, self.ref)
end
end)
end
function CategoryMenuView:didUpdate(previousProps, previousState)
if self.props.InFocus == previousProps.InFocus then
return
end
if self.props.InFocus and self.ref then
Utility.RemoveSelectionGroup(self.key)
Utility.AddSelectionParent(self.key, self.ref)
else
Utility.RemoveSelectionGroup(self.key)
end
end
function CategoryMenuView:willUnmount()
Utility.RemoveSelectionGroup(self.key)
end
return CategoryMenuView
@@ -0,0 +1,33 @@
--[[
Creates a vertical divider
Props:
Position : UDim2 - The position of the divider.
DividerWidth : UDim - The width of the divider.
DividerLength : UDim - The length of the divider.
Color : Color3 - The color of the divider. Default GlobalSettings.PageDivideColor
FillDirection : FillDirection - The direction of the divider.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
local color = props.Color or GlobalSettings.PageDivideColor
local dividerLength = props.DividerLength
local dividerWidth = props.DividerWidth or UDim.new(0,2)
local position = props.Position
local size
if props.FillDirection == Enum.FillDirection.Horizontal then
size = UDim2.new(dividerLength.Scale, dividerLength.Offset, dividerWidth.Scale, dividerWidth.Offset)
else
size = UDim2.new(dividerWidth.Scale, dividerWidth.Offset, dividerLength.Scale, dividerLength.Offset)
end
return Roact.createElement("Frame",{
BackgroundColor3 = color,
BorderSizePixel = 0,
Size = size,
Position = position,
})
end
@@ -0,0 +1,149 @@
--[[
A component that is used for navigation and redirects.
Note: The parent must be a scolling
Props:
Key : String
InFocus : bool
Scale : Vector2
RedirectBack : function()
RedirectLeft : function(Ref<GuiObject>) - If nil,
RedirectRight : function(Ref<GuiObject>)
RedirectUp : function(Ref<GuiObject>)
RedirectDown : function(Ref<GuiObject>)
ActionPriority : The action priority
]]
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local ContextActionService = game:GetService("ContextActionService")
local RedirectComponent = Roact.PureComponent:extend("RedirectComponent")
local BACK_KEY = Enum.KeyCode.ButtonB
local DEFAULT_ACTION_PRIORITY = 2000
local function addBackButton(self)
self.exitActionName = "ExitSection"..self.Key
ContextActionService:UnbindCoreAction(self.exitActionName)
if self.props.ActionPriority then
ContextActionService:BindCoreActionAtPriority(self.exitActionName, self.exitAction, false, DEFAULT_ACTION_PRIORITY + self.props.ActionPriority, BACK_KEY)
else
ContextActionService:BindCoreAction(self.exitActionName, self.exitAction, false, BACK_KEY)
end
end
local function removeBackButton(self)
if self.exitActionName ~= nil then
ContextActionService:UnbindCoreAction(self.exitActionName)
self.exitActionName = nil
end
end
function RedirectComponent:init()
self.Key = self.props.Key or "RedirectComponent"
self.exitActionName = nil
self.backPressed = false
self.exitAction = function(actionName, inputState, inputObject)
if not self.props.InFocus then
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
self.backPressed = true
elseif inputState == Enum.UserInputState.End and self.backPressed then
self.backPressed = false
self.props.RedirectBack()
end
end
end
function RedirectComponent:render()
local props = self.props
local scale = props.Scale or Vector2.new(1,1)
if props.InFocus and props.RedirectBack then
addBackButton(self)
else
removeBackButton(self)
end
local redirectLeftButton;
if props.InFocus and props.RedirectLeft then
redirectLeftButton = Roact.createElement('TextButton',
{
Position = UDim2.new(-scale.X/2, -1, 0.5, 0),
Size = UDim2.new(0, 2, 1+scale.Y, 0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectLeft(rbx)
end,
})
end
local redirectRightButton;
if props.InFocus and props.RedirectRight then
redirectRightButton = Roact.createElement('TextButton',
{
Position = UDim2.new(1+scale.X/2, 1, 0.5, 0),
Size = UDim2.new(0, 2, 1+scale.Y, 0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectRight(rbx)
end,
})
end
local redirectUpButton;
if props.InFocus and props.RedirectUp then
redirectUpButton = Roact.createElement('TextButton',
{
Position = UDim2.new(0.5, 0, -scale.Y/2, -1),
Size = UDim2.new(1+scale.X, 0, 0, 2),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectUp(rbx)
end,
})
end
local redirectDownButton;
if props.InFocus and props.RedirectDown then
redirectDownButton = Roact.createElement('TextButton',
{
Position = UDim2.new(0.5, 0, 1+scale.Y/2, 1),
Size = UDim2.new(1+scale.X, 0, 0, 2),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Text = "",
[Roact.Event.SelectionGained] = function(rbx)
props.RedirectDown(rbx)
end,
})
end
return Roact.createElement('ScrollingFrame',
{
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(0.5,0.5),
BackgroundTransparency = 1,
Selectable = false,
ScrollingEnabled = false,
ScrollBarThickness = 0,
CanvasSize = UDim2.new(0, 0, 1, 0),
},{
RedirectLeftButton = redirectLeftButton,
RedirectRightButton = redirectRightButton,
RedirectUpButton = redirectUpButton,
RedirectDownButton = redirectDownButton,
})
end
function RedirectComponent:willUnmount()
removeBackButton(self)
end
return RedirectComponent
@@ -0,0 +1,206 @@
--[[
Creates a Roact component that is a rounded button
Props:
Button : dictionary - Config for the button.
.Image : Content - The image of the button.
.Size : UDim2 - Size of the button.
.Position : UDim2 - Position of the button.
.AnchorPoint : UDim2 - The anchor point of the button.
.ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back.
.LayoutOrder: int - Controls the sorting priority of this button.
.Selectable : bool - Whether or not this object should be selectable using joysticks (controller).
Text : dictionary - A map of props for the text
.Text : string - The label of the button.
.Size : UDim2 - Size of the button.
.Position : UDim2 - Position of the button.
.AnchorPoint : The anchor point of the button.
.Font : Font - The font used to display the given text.
.TextSize : float - The font size in pixels.
.TextXAlignment : TextXAlignment - Sets where text is placed on the X axis within the TextLabel.
.ZIndex: int - Determines the order in which GUI objects are rendered, with 10 being in front and 1 in back.
Focused : bool - Is the button in focus.
Disabled : bool - Is the button disabled. By default a disabled button will not be selectable.
Selected : bool - Should the button be selected.
OnSelectionGained : bool function() -
Fired when the GuiObject is being focused on with the Gamepad selector.
Return true if it should be focused. False otherwise.
OnSelectionLost : bool function() -
Fired when the Gamepad selector stops focusing on the GuiObject.
Return false if it should be un-focused. True otherwise.
OnActivated : function() - Fires when the button is activated.
HideSelectionImage : bool - Whether or not to hide the selection object
DefaultProps : dictionary a map for the default props of the button.
.ImageColor3 : Color3
.ImageTransparency : float
.TextColor3 : Color3
FocusedProps : dictionary a map for the focused props of the button.
.ImageColor3 : Color3
.ImageTransparency : float
.TextColor3 : Color3
DisabledProps : dictionary a map for the disabled props of the button.
.ImageColor3 : Color3
.ImageTransparency : float
.TextColor3 : Color3
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Immutable = require(Modules.Common.Immutable)
local Utility = require(Modules.Shell.Utility)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
local RoundedButton = Roact.PureComponent:extend("RoundedButton")
function RoundedButton:init()
self.selectionImageObject = Utility.Create "ImageLabel"
{
Name = "SelectorImage",
Image = GlobalSettings.Images.ButtonSelector,
Position = UDim2.new(0, -7, 0, -7),
Size = UDim2.new(1, 14, 1, 14),
BackgroundTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(31, 31, 63, 63),
}
self.defaultProps =
{
ImageColor3 = GlobalSettings.Colors.WhiteButton,
ImageTransparency = 0.8,
TextColor3 = GlobalSettings.Colors.WhiteText,
}
self.focusedProps =
{
ImageColor3 = GlobalSettings.Colors.BlueButton,
ImageTransparency = 0,
TextColor3 = GlobalSettings.Colors.TextSelected,
}
self.disabledProps =
{
ImageColor3 = GlobalSettings.Colors.WhiteButton,
ImageTransparency = 1,
TextColor3 = GlobalSettings.Colors.WhiteText,
}
self.buttonImage = GlobalSettings.Images.ButtonDefault
--TODO: Change to new Ref API
self.onCreate = function(rbx)
self.ref = rbx
end
end
function RoundedButton:render()
local button = self.props.Button or {}
local text = self.props.Text or {}
local inputDefaultProps = self.props.DefaultProps or {}
local defaultProps = {}
for k in pairs(self.defaultProps) do
defaultProps[k] = inputDefaultProps[k] or self.defaultProps[k]
end
local inputFocusedProps = self.props.FocusedProps or {}
local focusedProps = {}
for k in pairs(self.focusedProps) do
focusedProps[k] = inputFocusedProps[k] or self.focusedProps[k]
end
local inputDisabledProps = self.props.DisabledProps or {}
local disabledProps = {}
for k in pairs(self.disabledProps) do
disabledProps[k] = inputDisabledProps[k] or self.disabledProps[k]
end
if self.props.HideSelectionImage then
self.selectionImageObject.Visible = false
else
self.selectionImageObject.Visible = true
end
local selectable = true
if button.Selectable == false then
selectable = false
end
local currentProps = defaultProps
if self.props.Disabled then
currentProps = disabledProps
selectable = button.Selectable or false
elseif self.props.Focused then
currentProps = focusedProps
end
local baseButtonProps =
{
Image = self.buttonImage,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Selectable = selectable,
[Roact.Ref] = self.onCreate,
[Roact.Event.SelectionGained] = self.props.OnSelectionGained,
[Roact.Event.SelectionLost] = self.props.OnSelectionLost,
[Roact.Event.Activated] = self.props.OnActivated,
ImageColor3 = currentProps.ImageColor3,
ImageTransparency = currentProps.ImageTransparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
SelectionImageObject = self.selectionImageObject,
BackgroundTransparency = 1,
}
local buttonProps = Immutable.JoinDictionaries(baseButtonProps, button)
local baseTextProps =
{
Text = "",
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Font = GlobalSettings.Fonts.Regular,
TextSize = GlobalSettings.TextSizes.Button,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = currentProps.TextColor3,
TextTransparency = currentProps.TextTransparency,
BackgroundTransparency = 1,
}
local textProps = Immutable.JoinDictionaries(baseTextProps, text)
local textLabel = Roact.createElement("TextLabel", textProps)
local moveSelection = Roact.createElement(SoundComponent,
{
SoundName = "MoveSelection",
}
)
local children = self.props[Roact.Children] or {}
return Roact.createElement("ImageButton",
buttonProps,
Immutable.JoinDictionaries(
{
Label = textLabel,
MoveSelection = moveSelection,
}, children)
)
end
function RoundedButton:didMount()
delay(0, function()
if self.props.Selected then
Utility.SetSelectedCoreObject(self.ref)
end
end)
end
function RoundedButton:didUpdate(previousProps, previousState)
if not previousProps.Selected and self.props.Selected then
Utility.SetSelectedCoreObject(self.ref)
end
end
return RoundedButton
@@ -0,0 +1,109 @@
--[[
Creates a Roact component that is a category selector button
Props:
Key : Variant - The key for the selector button.
Text : String - The label of the button.
Size : UDim2 - The size of the this button.
AnchorPoint : Vector2 - The anchor point of the button.
Position : UDim2 - The position of the button.
LayoutOrder : int - The layout order of the button.
Focused : bool - Is the button in focus.
Disabled : bool - Is the button disabled. By default a disabled button will not be selectable.
Selected : bool - Should the button be selected.
OnSelectionGained : function(key : Variant) - Fired when the GuiObject
is being focused on with the Gamepad selector.
OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject.
OnActivated : function(key : Variant) - Fires when the button is activated.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Immutable = require(Modules.Common.Immutable)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
local SelectorButton = Roact.PureComponent:extend("SelectorButton")
local SELECTOR_ICON_SIZE_X = 15
local SELECTOR_ICON_SIZE_Y = 30
local SELECTOR_OFFSET_X = 14
function SelectorButton:init()
self.key = self.props.Key
self.onSelectionGained = function()
if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end
end
self.onSelectionLost = function()
if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end
end
self.onActivated = function()
if self.props.OnActivated then self.props.OnActivated(self.key) end
end
end
function SelectorButton:render()
local buttonProps = {}
local textProps = {}
local size = self.props.Size
local position = self.props.Position
local anchorPoint = self.props.AnchorPoint
local focused = self.props.Focused
local disabled = self.props.Disabled
local selected = self.props.Selected
self.layoutOrder = self.props.LayoutOrder
buttonProps.Size = UDim2.new(1, 0, 1, 0)
buttonProps.AnchorPoint = Vector2.new(0.5, 0.5)
buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0)
textProps.Text = self.props.Text
textProps.Size = UDim2.new(1, 0, 1, 0)
textProps.AnchorPoint = Vector2.new(0, 0.5)
textProps.Position = UDim2.new(0, 24, 0.5, 0)
local button = Roact.createElement(RoundedButton,
{
Button = buttonProps,
Text = textProps,
Focused = focused,
Disabled = disabled,
Selected = selected,
OnSelectionGained = self.onSelectionGained,
OnSelectionLost = self.onSelectionLost,
OnActivated = self.onActivated,
})
local selector;
if focused == true then
selector = Roact.createElement("ImageLabel",
{
Size = UDim2.new(0, SELECTOR_ICON_SIZE_X, 0, SELECTOR_ICON_SIZE_Y),
Image = GlobalSettings.Images.RightArrow,
Position = UDim2.new(1, SELECTOR_OFFSET_X, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
BackgroundTransparency = 1,
})
end
local children = self.props[Roact.Children] or {}
return Roact.createElement("Frame",
{
Size = size,
Position = position,
AnchorPoint = anchorPoint,
LayoutOrder = self.layoutOrder,
BackgroundTransparency = 1,
},Immutable.JoinDictionaries(
{
Button = button,
Selector = selector,
}, children)
)
end
return SelectorButton
@@ -0,0 +1,20 @@
--[[
Creates a component with a shadow image
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
return Roact.createElement("ImageLabel", {
Name = 'Shadow',
Image = GlobalSettings.Images.Shadow,
Size = UDim2.new(1,3,1,3),
Position = UDim2.new(0,0,0,0),
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(10,10,28,28),
BackgroundTransparency = 1,
ZIndex = props.ZIndex or 1,
})
end
@@ -0,0 +1,260 @@
--[[
Creates a component for sidebar
Props:
buttons : array An array of the buttons to be added on the sidebar.
key - string The text to be shown on the sidebar button.
value - function() A callback which will be called when the button is activated.
text: string - The text to be shown on the sidebar.
inFocus: bool - The boolean which indicates whether the sidebar is open.
selectIndex: int - The button index we try to select when the sidebar is open.
paddingTop : UDim - The padding to apply on the top side relative to the sidebar's normal size.
paddingBottom : UDim - The padding to apply on the bottom side relative to the sidebar's normal size.
displayOrder: int - The order that the sidebar ScreenGui is drawn.
onRemoveFocus : function() - Callback function when the remove focus from sidebar
onClose: function() - Callback function when the sidebar is closed.
actionPriority : int - The action priority on sidebar.
]]
local CoreGui = game:GetService("CoreGui")
local Modules = CoreGui.RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoactMotion = require(Modules.LuaApp.RoactMotion)
local SoundManager = require(Modules.Shell.SoundManager)
local Components = Modules.Shell.Components
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
local ContextActionEvent = require(Components.ContextActionEvent)
local Utility = require(Modules.Shell.Utility)
local INSET_X = 65
local BUTTON_SIZE_Y = 75
local SIDEBAR_SELECTION_GROUP_NAME = "SideBar"
local SideBar = Roact.PureComponent:extend("SideBar")
function SideBar:init()
self.groupKey = SIDEBAR_SELECTION_GROUP_NAME
self.buttonImage = GlobalSettings.Images.ButtonDefault
self.selectionImageObject = Utility.Create "ImageLabel"({
Name = "SelectorImage",
BackgroundTransparency = 1,
Visible = false
})
self.defaultProps = {
buttonColor3 = GlobalSettings.Colors.WhiteButton,
buttonTransparency = 1,
textColor3 = GlobalSettings.Colors.WhiteText,
}
self.focusedProps = {
buttonColor3 = GlobalSettings.Colors.BlueButton,
buttonTransparency = 0,
textColor3 = GlobalSettings.Colors.TextSelected,
}
end
function SideBar:render()
local props = self.props
local onClose = function()
self.buttons = {}
Utility.RemoveSelectionGroup(self.groupKey)
if props.onClose then
props.onClose()
end
end
local contents = {
--Make it inside the title safe container
UIPadding = Roact.createElement("UIPadding", {
PaddingTop = props.paddingTop or UDim.new(0, 156),
PaddingBottom = props.paddingBottom or UDim.new(0, 39)
})
}
if props.buttons then
local index = 0
for _, buttonObj in ipairs(props.buttons) do
index = index + 1
local btIndex = index
local focused = self.state.selectedIndex and self.state.selectedIndex == btIndex
local currProps = focused and self.focusedProps or self.defaultProps
local btText = Roact.createElement("TextLabel", {
Size = UDim2.new(1, -INSET_X, 1, 0),
Position = UDim2.new(0, INSET_X, 0, 0),
AnchorPoint = Vector2.new(0, 0),
Text = buttonObj.text,
TextSize = GlobalSettings.TextSizes.Medium,
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = currProps.textColor3,
Font = GlobalSettings.RegularFont,
BackgroundTransparency = 1,
})
local moveSelection = Roact.createElement(SoundComponent, {
SoundName = "MoveSelection",
})
contents["Button"..btIndex] = Roact.createElement("ImageButton", {
Image = self.buttonImage,
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(1, -1, 0, BUTTON_SIZE_Y),
LayoutOrder = btIndex,
ImageColor3 = currProps.buttonColor3,
ImageTransparency = currProps.buttonTransparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
SelectionImageObject = self.selectionImageObject,
BackgroundTransparency = 1,
[Roact.Event.SelectionGained] = function()
self:setState({
selectedIndex = btIndex
})
end,
[Roact.Event.SelectionLost] = function()
self:setState({
selectedIndex = Roact.None
})
end,
[Roact.Event.Activated] = function()
SoundManager:Play("ButtonPress")
onClose()
buttonObj.callback()
end,
[Roact.Ref] = function(bt)
self.buttons = self.buttons or {}
self.buttons[btIndex] = bt
end,
}, {
ButtonText = btText,
MoveSelection = moveSelection,
})
end
if index > 0 then
contents.UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, 0),
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Top,
})
end
else
contents.TextLabel = Roact.createElement("TextLabel", {
Size = UDim2.new(1, -INSET_X - 100, 1, 0),
Position = UDim2.new(0, INSET_X, 0, 0),
BorderSizePixel = 0,
BackgroundTransparency = 1,
Text = props.text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextColor3 = GlobalSettings.WhiteTextColor,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.DescriptionSize,
TextWrapped = true,
})
end
local inFocus = props.inFocus
local modalBackgroundTransparency = inFocus and GlobalSettings.ModalBackgroundTransparency or 1
local containerPositionXScale = inFocus and 0.7 or 1
if not inFocus then
self.seenPressed = false
end
return Roact.createElement(RoactMotion.SimpleMotion, {
defaultStyle = {
modalBackgroundTransparency = 1,
containerPositionXScale = 1,
},
style = {
modalBackgroundTransparency = RoactMotion.spring(modalBackgroundTransparency, 600, 60),
containerPositionXScale = RoactMotion.spring(containerPositionXScale, 600, 60),
},
onRested = not inFocus and onClose,
render = function(values)
return Roact.createElement(Roact.Portal, { target = CoreGui }, {
SideBarGui = Roact.createElement("ScreenGui", {
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
DisplayOrder = props.displayOrder or 1
}, {
BackConnector = inFocus and Roact.createElement(ContextActionEvent, {
name = "CloseSideBar",
callback = function(actionName, inputState, inputObject)
if inputObject.KeyCode == Enum.KeyCode.ButtonB then
if inputState == Enum.UserInputState.Begin then
self.seenPressed = true
elseif inputState == Enum.UserInputState.End and self.seenPressed then
self:setState({
selectedIndex = Roact.None
})
if props.onRemoveFocus then
props.onRemoveFocus()
end
end
end
end,
binds = { Enum.UserInputType.Gamepad1, Enum.UserInputType.Gamepad2, Enum.UserInputType.Gamepad3, Enum.UserInputType.Gamepad4 },
actionPriority = props.actionPriority
}),
ModalOverlay = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = values.modalBackgroundTransparency,
BackgroundColor3 = GlobalSettings.ModalBackgroundColor,
BorderSizePixel = 0,
}, {
SideBarContainer = Roact.createElement("Frame", {
Size = UDim2.new(0.3, 0, 1, 0),
Position = UDim2.new(values.containerPositionXScale, 0, 0, 0),
BorderSizePixel = 0,
BackgroundColor3 = GlobalSettings.OverlayColor,
[Roact.Ref] = function(container)
self.container = container
end
}, contents)
})
}),
})
end
})
end
function SideBar:didMount()
delay(0, function()
if self.props.inFocus and self.container then
Utility.RemoveSelectionGroup(self.groupKey)
Utility.AddSelectionParent(self.groupKey, self.container)
local trySelectIndex = self.props.selectIndex or 1
if self.buttons and self.buttons[trySelectIndex] then
if not self.state.selectedIndex then
Utility.SetSelectedCoreObject(self.buttons[trySelectIndex])
end
else
Utility.SetSelectedCoreObject(nil)
end
end
end)
end
function SideBar:didUpdate(previousProps, previousState)
if self.props.inFocus == previousProps.inFocus then
return
end
if self.props.inFocus and self.container then
Utility.RemoveSelectionGroup(self.groupKey)
Utility.AddSelectionParent(self.groupKey, self.container)
local trySelectIndex = self.props.selectIndex or 1
if self.buttons and self.buttons[trySelectIndex] then
if not self.state.selectedIndex then
Utility.SetSelectedCoreObject(self.buttons[trySelectIndex])
end
else
Utility.SetSelectedCoreObject(nil)
end
else
Utility.RemoveSelectionGroup(self.groupKey)
end
end
function SideBar:willUnmount()
Utility.RemoveSelectionGroup(self.groupKey)
end
return SideBar
@@ -0,0 +1,20 @@
--[[
A simple sound component
Props:
SoundName : string - The name of the sound
]]
local RobloxGui = game:GetService("CoreGui").RobloxGui
local Modules = RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
local soundName = props.SoundName
local soundsUrl = GlobalSettings.Sounds[soundName]
return Roact.createElement('Sound',
{
SoundId = soundsUrl
})
end
@@ -0,0 +1,49 @@
--[[
Creates a component as a spinner
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Components = Modules.Shell.Components
local RenderStep = require(Components.RenderStep)
local SPINNER_IMAGE = 'rbxasset://textures/ui/Shell/Icons/LoadingSpinner@1080.png'
local Spinner = Roact.PureComponent:extend("Spinner")
function Spinner:init()
self.state = {
rotation = 0
}
self.speed = 360
self.update = function(dt)
self:setState({
rotation = self.state.rotation + dt * self.speed
})
end
end
function Spinner:render()
local props = self.props
local state = self.state
local rotation = state.rotation
self.speed = props.speed or 360
return Roact.createElement("ImageLabel", {
Rotation = rotation,
BackgroundTransparency = 1,
Size = props.Size or UDim2.new(0, 100, 0, 100),
AnchorPoint = props.AnchorPoint or Vector2.new(0.5, 0.5),
Position = props.Position or UDim2.new(0.5, 0, 0.5, 0),
Image = SPINNER_IMAGE,
ZIndex = props.ZIndex or 10,
ImageTransparency = props.ImageTransparency or 0,
},{
Render = Roact.createElement(RenderStep, {
name = tick(),
priority = Enum.RenderPriority.Input.Value,
callback = self.update,
}),
})
end
return Spinner
@@ -0,0 +1,54 @@
--[[
Creates a Roact component provides a left and a right view
Props:
Bias : number (0 - 1) - The bias for the size of left view vs the right view
LeftView : Roact.Component - The left roact element that will be a child of the SplitView
RightView : Roact.Component - The right roact element that will be a child of the SplitView
Divider : Roact.Component - The divide roact element that will divide the two sides.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local SplitViewLR = Roact.Component:extend("SplitViewLR")
function SplitViewLR:init()
end
function SplitViewLR:render()
local props = self.props
local bias = props.Bias
self.leftView = props.LeftView
self.rightView = props.RightView
self.divider = props.Divider
return Roact.createElement("Frame",
{
Size = UDim2.new(1,0,1,0),
BackgroundTransparency = 1,
},{
LeftFrame = Roact.createElement("Frame",
{
Size = UDim2.new(bias,0,1,0),
Position = UDim2.new(0,0,0,0),
BackgroundTransparency = 1,
},{Left = self.leftView}),
Divider = Roact.createElement("Frame",
{
Size = UDim2.new(0,0,0,0),
Position = UDim2.new(bias,0,0,0),
BackgroundTransparency = 1,
},{Divider = self.divider}),
RightFrame = Roact.createElement("Frame",
{
Size = UDim2.new(1-bias,0,1,0),
Position = UDim2.new(bias,0,0,0),
BackgroundTransparency = 1,
},{
Right = self.rightView
}),
})
end
return SplitViewLR
@@ -0,0 +1,116 @@
--[[
Create a Roact toggle button
Props:
Key : Variant - The key for the toggle button.
Text : String - The text of the button.
Size : UDim2 - The size of the this button.
AnchorPoint : Vector2 - The anchor point of the button.
Position : UDim2 - The position of the button.
Toggle : bool - Whether or not the button is toggled or not.
Focused : bool - Is the button selected.
Disabled : bool - Whether or not this button is disabled.
Selected : bool - Should the button be selected.
Selectable : bool - Whether or not the button is selectable.
OnSelectionGained : function(key : Variant) - Fired when the GuiObject is being focused on with the Gamepad selector.
OnSelectionLost : function(key : Variant) - Fired when the Gamepad selector stops focusing on the GuiObject.
OnActivated : function() - Fires when the button is activated.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Immutable = require(Modules.Common.Immutable)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoundedButton = require(Modules.Shell.Components.Common.RoundedButton)
local ToggleButton = Roact.Component:extend("ToggleButton")
local TOGGLE_ICON_SIZE = 32
local TOGGLE_ICON_OFFSET_X = 20
function ToggleButton:init()
self.key = self.props.Key
self.state =
{
selected = false,
active = false,
}
self.onSelectionGained = function()
if self.props.OnSelectionGained then self.props.OnSelectionGained(self.key) end
end
self.onSelectionLost = function()
if self.props.OnSelectionLost then self.props.OnSelectionLost(self.key) end
end
self.onActivated = function()
if self.props.OnActivated then self.props.OnActivated() end
end
end
function ToggleButton:render()
local buttonProps = {}
local textProps = {}
local size = self.props.Size
local position = self.props.Position
local anchorPoint = self.props.AnchorPoint
local focused = self.props.Focused
local disabled = self.props.Disabled
local selected = self.props.Selected
--Default is unknown since its not on or off
local iconColor = GlobalSettings.Colors.StatusIconUnknown
if self.props.Toggle == true then
iconColor = GlobalSettings.Colors.StatusIconEnabled
elseif self.props.Toggle == false then
iconColor = GlobalSettings.Colors.StatusIconDisabled
end
buttonProps.Size = UDim2.new(1, 0, 1, 0)
buttonProps.AnchorPoint = Vector2.new(0.5, 0.5)
buttonProps.Position = UDim2.new(0.5, 0, 0.5, 0)
buttonProps.Selectable = self.props.Selectable
textProps.Text = self.props.Text
textProps.Size = UDim2.new(1, 0, 1, 0)
textProps.AnchorPoint = Vector2.new(0, 0.5)
textProps.Position = UDim2.new(0, TOGGLE_ICON_SIZE+2*TOGGLE_ICON_OFFSET_X, 0.5, 0)
local toggleImage = Roact.createElement("ImageLabel",
{
Size = UDim2.new(0, TOGGLE_ICON_SIZE, 0, TOGGLE_ICON_SIZE),
Image = GlobalSettings.Images.EnabledStatusIcon,
ImageColor3 = iconColor,
Position = UDim2.new(0, TOGGLE_ICON_OFFSET_X, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
BackgroundTransparency = 1,
})
local button = Roact.createElement(RoundedButton,
{
Button = buttonProps,
Text = textProps,
Focused = focused,
Disabled = disabled,
Selected = selected,
OnSelectionGained = self.onSelectionGained,
OnSelectionLost = self.onSelectionLost,
OnActivated = self.onActivated,
},{
ToggleImage = toggleImage,
})
local children = self.props[Roact.Children] or {}
return Roact.createElement("Frame",
{
Size = size,
Position = position,
AnchorPoint = anchorPoint,
BackgroundTransparency = 1,
}, Immutable.JoinDictionaries(
{
Button = button,
}, children))
end
return ToggleButton
@@ -0,0 +1,92 @@
--[[
Creates a Image Loader
Props:
rbxuid : int - Roblox user id.
thumbnailType : Enum.ThumbnailType - Describes the type of user thumbnail that should be returned by GetUserThumbnailAsync.
thumbnailSize : Enum.ThumbnailSize - Describes the resolution of a user thumbnail being returned by GetUserThumbnailAsync.
Size : UDim2 - The thumbnail image size.
Position : UDim2 - The thumbnail image position.
BackgroundTransparency : float - Transparency of the thumbnail image background.
BackgroundColor3 : Color3 - Color of the thumbnail image background.
hasThumbnailData : bool - Whether we have the corresponding thumbnail data in store.
imageUrl : string - The imageUrl for the thumbnail.
isFetching : bool - Whether we are fetching the thumbnail.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Components = Modules.Shell.Components
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local RoactRodux = require(Modules.Common.RoactRodux)
local ApiFetchUserThumbnail = require(Modules.Shell.Thunks.ApiFetchUserThumbnail)
local Spinner = require(Components.Common.Spinner)
local memoize = require(Modules.Common.memoize)
local RETRIES = 6
local UserThumbnailLoader = Roact.PureComponent:extend("UserThumbnailLoader")
function UserThumbnailLoader:render()
local props = self.props
local rbxuid = props.rbxuid
local children = {}
local imageUrl = ""
if rbxuid and rbxuid > 0 then
local hasThumbnailData = props.hasThumbnailData
--TODO: Try refetch if last fetched failed after some interval
if hasThumbnailData and props.imageUrl then
imageUrl = props.imageUrl
else
props.fetchImage(rbxuid, props.thumbnailType, props.thumbnailSize)
end
if props.showSpinner then
children.Spinner = props.isFetching and Roact.createElement(Spinner)
end
else
children.XboxDefaultProfileImage = Roact.createElement("ImageLabel", {
Size = UDim2.new(0.5, 0, 0.5, 0),
Position = UDim2.new(0.25, 0, 0.25, 0),
BackgroundTransparency = 1,
Image = GlobalSettings.Images.DefaultProfile,
})
end
return Roact.createElement("ImageLabel", {
Image = imageUrl,
Size = props.size or UDim2.new(1, 0, 1, 0),
Position = props.position or UDim2.new(0, 0, 0, 0),
BackgroundTransparency = props.backgroundTransparency or 0,
BorderSizePixel = 0,
BackgroundColor3 = props.backgroundColor3 or GlobalSettings.Colors.CharacterBackground,
}, children)
end
local getThumbnailData = memoize(function(thumbnailData)
return {
hasThumbnailData = thumbnailData ~= nil,
isFetching = thumbnailData and thumbnailData.isFetching,
imageUrl = thumbnailData and thumbnailData.imageUrl,
}
end)
local function mapStateToProps(state, props)
local rbxuid = props.rbxuid
local thumbnailType = props.thumbnailType
local thumbnailSize = props.thumbnailSize
local thumbnailData;
if rbxuid and rbxuid > 0 and thumbnailType and thumbnailSize then
local thumbnailId = table.concat{ rbxuid, thumbnailType.Name, thumbnailSize.Name }
thumbnailData = state.UserThumbnails[thumbnailId]
end
return getThumbnailData(thumbnailData)
end
local function mapDispatchToProps(dispatch)
return {
fetchImage = function(rbxuid, thumbnailType, thumbnailSize)
return dispatch(ApiFetchUserThumbnail(rbxuid, thumbnailType, thumbnailSize, RETRIES))
end
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps, mapDispatchToProps)(UserThumbnailLoader)
@@ -0,0 +1,204 @@
--[[
Allows the selector to navigate between different pages/levels.
How to use:
The Navigator should be created at the root of the pages/levels.
- Call EnterRootSection(rootKey) to set the root page of the Navigator.
- Call Set(pageKey, defaultGuiObj) to set the default selection object for the given pageKey.
- Call Destruct() when done using the Navigator to clean up the event connections.
To Enter a page:
1. Call SetNextPage(nextPageKey) to set the destination for the next page/level.
2. Call EnterSection() to enter the next selection page/level.
Set(pageKey, defaultGuiObj) needs to be called to set the default guiobject selection.
Otherwise, the selection will fail and this function will return false.
This function can be called anywhere to enter the next level.
Eg. - Call by a GuiObject on selection.
- Call by an event.
- Call by an action with Rodux.
To Exit a page and return to the previous:
1. Call ExitSection() to return to the previous page/level.
This function can be called from anywhere.
It will return false if it is unable to select the next that was set
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Utility = require(Modules.Shell.Utility)
local ContextActionService = game:GetService('ContextActionService')
local Nav = {}
Nav.__index = Nav
local function AddEnterButtonHelper(self,keycode)
local enterActionName = "EnterSection"..tostring(keycode)
self._actions.EnterSection[keycode] = enterActionName
ContextActionService:BindCoreAction(enterActionName,
function(actionName, inputState, inputObject)
return self:_enterAction(actionName, inputState, inputObject)
end
,false,keycode)
end
local function AddExitButtonHelper(self,keycode)
local exitActionName = "ExitSection"..tostring(keycode)
self._actions.ExitSection[keycode] = exitActionName
ContextActionService:BindCoreAction(exitActionName,
function(actionName, inputState, inputObject)
return self:_exitAction(actionName, inputState, inputObject)
end
,false,keycode)
end
-- By default, the ButtonB binds to back
-- Needs to be called before this object can be used.
function Nav.new()
local self = {}
self._levels = {}
self._guiobjs = {}
self._nextPageKey = ""
self._actions = {}
self._seenPressed = {}
self._actions.EnterSection = {}
self._actions.ExitSection = {}
AddExitButtonHelper(self, Enum.KeyCode.ButtonB)
setmetatable(self, Nav)
return self
end
--[[
Sets a custom key press to enter the next page.
args:
keycode : Enum.KeyCode - The key code of the button.
]]
function Nav:AddEnterButton(keycode)
AddEnterButtonHelper(self,keycode)
end
--[[
Sets a custom key press to exit to the previous page.
args:
keycode : Enum.KeyCode - The key code of the button.
]]
function Nav:AddExitButton(keycode)
AddExitButtonHelper(self,keycode)
end
--[[
Sets and enters the root page. Must be called before the navigator can be used.
args:
rootKey : Variant - The key for the page.
]]
function Nav:EnterRootSection(rootKey)
if self._guiobjs[rootKey] == nil then
return false
end
self._levels = {}
--Stack push to the first element
table.insert(self._levels,rootKey)
Utility.SetSelectedCoreObject(self._guiobjs[rootKey])
return true
end
--[[
Enters the next page
return:
True : if it can enter the next page.
False: otherwise.
]]
function Nav:EnterSection()
local nextPageKey = self._nextPageKey
if nextPageKey == nil or self._guiobjs[nextPageKey] == nil then
return false
end
--Stack push
table.insert(self._levels,nextPageKey)
Utility.SetSelectedCoreObject(self._guiobjs[nextPageKey])
self._nextPageKey = nil
return true
end
--[[
Exits to the previous page
return:
True : if it can enter the previous page
False: otherwise.
]]
function Nav:ExitSection()
--stack pop
table.remove(self._levels)
-- stack peak
local key = self._levels[#self._levels]
if self._guiobjs[key] == nil then
return false
end
Utility.SetSelectedCoreObject(self._guiobjs[key])
return true
end
function Nav:_enterAction(actionName, inputState, inputObject)
-- There is no next page to go to
if self._nextPageKey == nil then
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
self._seenPressed[inputObject.KeyCode] = true
return Enum.ContextActionResult.Sink
elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then
self._seenPressed[inputObject.KeyCode] = false
self:EnterSection()
return Enum.ContextActionResult.Sink
end
end
function Nav:_exitAction(actionName, inputState, inputObject)
--If we are current at the root page
if(#self._levels <= 1) then
-- There is nothing to go back to.
return Enum.ContextActionResult.Pass
end
if inputState == Enum.UserInputState.Begin then
self._seenPressed[inputObject.KeyCode] = true
elseif inputState == Enum.UserInputState.End and self._seenPressed[inputObject.KeyCode] then
self._seenPressed[inputObject.KeyCode] = false
self:ExitSection()
end
return Enum.ContextActionResult.Sink
end
--[[
Sets the default object to select.
args:
pageKey : Variant - The key for the page.
defaultGuiObj : GuiObject - the default selected object for the page.
]]
function Nav:Set(pageKey, defaultGuiObj)
self._guiobjs[pageKey] = defaultGuiObj
end
--[[
Sets the key of the next page.
args:
nextPageKey : Variant - The key for the next page.
]]
function Nav:SetNextPage(nextPageKey)
self._nextPageKey = nextPageKey
end
-- Needs to be called to clean up event connection.
function Nav:Destruct()
for _,v in pairs(self._actions.EnterSection) do
ContextActionService:UnbindCoreAction(v)
end
for _,v in pairs(self._actions.ExitSection) do
ContextActionService:UnbindCoreAction(v)
end
end
return Nav
@@ -0,0 +1,64 @@
--[[
Creates a Roact component provides a vertical list view for multiple elements
Props:
PaddingTop : UDim - The top padding for the list
PaddingBottom : UDim - The bottom padding for the list
Spacing : UDim - The spacing between each item of the list
HorizontalAlignment : HorizontalAlignment - Determines how grid is placed within
it's parent's container in the x direction.
Can be Left, Center, or Right.
ScrollingEnabled : bool - Determines whether or not scrolling is allowed on the frame.
If false, no scroll bars will be rendered.
ScrollBarThickness : number - How thick the scroll bar appears.
This applies to both the horizontal and vertical scroll bars.
If set to 0, no scroll bars are rendered.
CanvasPosition : Vector2 - The location within the canvas, in pixels,
that should be drawn at the top left of the scroll frame.
CanvasSize : UDim2 - Determines the size of the area that is scrollable.
The UDim2 is calculated using the parent gui's size,
similar to the regular Size property on gui objects.
Items: array<Roact.Component> - The items in the list view
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
return function(props)
local paddingTop = props.PaddingTop
local paddingBottom = props.PaddingBottom
local spacing = props.Spacing
local horizontalAlignment = props.HorizontalAlignment
local scrollBarThickness = props.ScrollBarThickness
local scrollingEnabled = props.ScrollingEnabled
local canvasPosition = props.CanvasPosition
local canvasSize = props.CanvasSize
local ListItems = {}
ListItems["UIPadding"] = Roact.createElement("UIPadding",
{
PaddingTop = paddingTop,
PaddingBottom = paddingBottom,
})
ListItems["UIListLayout"] = Roact.createElement("UIListLayout",
{
Padding = spacing,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = horizontalAlignment,
VerticalAlignment = Enum.VerticalAlignment.Top,
CanvasPosition = canvasPosition,
CanvasSize = canvasSize,
})
for k,v in pairs(props.Items) do
ListItems[k] = v
end
return Roact.createElement("ScrollingFrame",
{
Size = UDim2.new(1,0,1,0),
ScrollBarThickness = scrollBarThickness,
BackgroundTransparency = 1,
Selectable = false,
ScrollingEnabled = scrollingEnabled,
},ListItems)
end
@@ -0,0 +1,252 @@
--[[
Creates a Roact component for inifinite scrolling
Props:
scrollingFrameProps : dictionary - props for the scrolling frame
.Selectable : bool - Whether or not this object should be selectable using joysticks (controller).
.ClipsDescendants : bool - Determines whether Roblox will render any portions of its GUI descendants that are outside of its own borders.
items : Array - An array of the input item data which is used to construct item component.
itemSize : Vector2 - The size for each item in the scrolling frame.
itemsPaddingOffset : int - The padding between each item.
scrollingDirection : Enum.ScrollingDirection - The scrolling direction of the scrolling frame, can't be Enum.ScrollingDirection.XY.
itemOffsetStart: int - The minimum distance of the selected guiobject to the window start border.
itemOffsetEnd: int - The minimum distance of the selected guiobject to the window end border.
customScrollDist: dictionary - Custom distances to trigger the scroll.
generateKey : function() - Used to generate a name for the item.
renderItem : function() -
Input: item data, item index and an onSelectionGained callback
Output: a Roact Component
State:
viewStart: int - The item start index.
viewSize: int - The number of items can be put in the window.
paddingStart: int - The padding to apply on the top / left side of the scrolling frame.
]]
local GuiService = game:GetService("GuiService")
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Utility = require(Modules.Shell.Utility)
local Roact = require(Modules.Common.Roact)
local WindowedScrollingFrame = Roact.PureComponent:extend("WindowedScrollingFrame")
local function clipCanvasPosition(scrollingFrame, canvasPos)
local canvasPosX = canvasPos.X
local canvasPosY = canvasPos.Y
canvasPosX = math.min(canvasPosX, scrollingFrame.CanvasSize.X.Offset - scrollingFrame.AbsoluteWindowSize.X)
canvasPosX = math.max(0, canvasPosX)
canvasPosY = math.min(canvasPosY, scrollingFrame.CanvasSize.Y.Offset - scrollingFrame.AbsoluteWindowSize.Y)
canvasPosY = math.max(0, canvasPosY)
return Vector2.new(canvasPosX, canvasPosY)
end
function WindowedScrollingFrame:init()
self.state = {
viewStart = 0,
viewSize = 0,
paddingStart = 0,
}
self.scrollingFrameRef = function(rbx)
self.scrollingFrame = rbx
end
end
function WindowedScrollingFrame:onSelectionChanged(selectedItem)
if not self.scrollingFrame then
return
end
if selectedItem == nil or selectedItem == self.savedSelectedObject or not selectedItem:IsDescendantOf(self.scrollingFrame) then
return
end
self.savedSelectedObject = selectedItem
local scrollingFrame = self.scrollingFrame
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
local canvasPosition = scrollingFrame.CanvasPosition
local axisKey = "X"
if scrollingDirection == Enum.ScrollingDirection.Y then
axisKey = "Y"
end
-- If our scrolling frame has zero height / width, let's not bother trying to
-- recompute our sizing
if absoluteWindowSize[axisKey] == 0 then
return
end
local itemOffsetStart = self.props.itemOffsetStart or 0
local itemOffsetEnd = self.props.itemOffsetEnd or 0
local customScrollDist = self.props.customScrollDist or {}
--If the selected guiobject is off-window, we move it back into the window instantly
--Then make the motion
local instantPos;
local tweenTargetPos;
if scrollingDirection == Enum.ScrollingDirection.Y then
local topDistance = selectedItem.AbsolutePosition.Y - scrollingFrame.AbsolutePosition.Y
local bottomDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).Y
local minDistTop = itemOffsetStart
local minDistBottom = itemOffsetEnd
if topDistance < (customScrollDist.Top or minDistTop) then
if topDistance < 0 then
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y + topDistance)
end
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y - (minDistTop - topDistance))
elseif bottomDistance < (customScrollDist.Bottom or minDistBottom) then
if bottomDistance < 0 then
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y - bottomDistance)
end
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y + minDistBottom - bottomDistance)
end
elseif scrollingDirection == Enum.ScrollingDirection.X then
local leftDistance = selectedItem.AbsolutePosition.X - scrollingFrame.AbsolutePosition.X
local rightDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).X
local minDistLeft = itemOffsetStart
local minDistRight = itemOffsetEnd
if leftDistance < (customScrollDist.Left or minDistLeft) then
if leftDistance < 0 then
instantPos = Vector2.new(canvasPosition.X + leftDistance, canvasPosition.Y)
end
tweenTargetPos = Vector2.new(canvasPosition.X - (minDistLeft - leftDistance), canvasPosition.Y)
elseif rightDistance < (customScrollDist.Right or minDistRight) then
if rightDistance < 0 then
instantPos = Vector2.new(canvasPosition.X - rightDistance, canvasPosition.Y)
end
tweenTargetPos = Vector2.new(canvasPosition.X + minDistRight - rightDistance, canvasPosition.Y)
end
end
if instantPos then
instantPos = clipCanvasPosition(scrollingFrame, instantPos)
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, instantPos, 0, Utility.EaseOutQuad, true, function()
if tweenTargetPos then
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
end
end)
end
if not instantPos and tweenTargetPos then
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", canvasPosition, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
end
end
function WindowedScrollingFrame:updateViewBounds()
if not self.scrollingFrame then
return
end
local scrollingFrame = self.scrollingFrame
local itemSize = self.props.itemSize
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
local canvasPosition = scrollingFrame.CanvasPosition
local axisKey = "X"
if scrollingDirection == Enum.ScrollingDirection.Y then
axisKey = "Y"
end
-- If our scrolling frame has zero height / width, let's not bother trying to
-- recompute our sizing
if absoluteWindowSize[axisKey] == 0 then
return
end
canvasPosition = clipCanvasPosition(scrollingFrame, canvasPosition)
local itemTotalSize = (itemSize[axisKey] + itemsPaddingOffset)
local viewSize = math.ceil(absoluteWindowSize[axisKey] / itemTotalSize) + 1
local viewStart = math.floor(canvasPosition[axisKey] / itemTotalSize)
local paddingStart = math.max(0, (viewStart - 1) * itemTotalSize)
local shouldUpdate = viewSize ~= self.state.viewSize or viewStart ~= self.state.viewStart or paddingStart ~= self.state.paddingStart
if shouldUpdate then
self:setState({
viewStart = viewStart,
viewSize = viewSize,
paddingStart = paddingStart,
})
end
end
function WindowedScrollingFrame:render()
local items = self.props.items
local generateKey = self.props.generateKey
local renderItem = self.props.renderItem
local itemSize = self.props.itemSize
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
assert(scrollingDirection ~= Enum.ScrollingDirection.XY, "Can't set ScrollingDirection as XY.")
local children = {}
children.UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, itemsPaddingOffset),
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = scrollingDirection == Enum.ScrollingDirection.Y and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal
})
if scrollingDirection == Enum.ScrollingDirection.Y then
children.UIPadding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, self.state.paddingStart)
})
elseif scrollingDirection == Enum.ScrollingDirection.X then
children.UIPadding = Roact.createElement("UIPadding", {
PaddingLeft = UDim.new(0, self.state.paddingStart)
})
end
local lowerBound = math.max(1, self.state.viewStart)
local upperBound = math.min(#items, self.state.viewStart + self.state.viewSize)
for i = lowerBound, upperBound do
local key = generateKey and generateKey(i) or i
children[key] = renderItem(items[i], i)
end
local scrollingFrameProps = self.props.scrollingFrameProps or {}
local canvasSize = nil
if scrollingDirection == Enum.ScrollingDirection.Y then
canvasSize = UDim2.new(1, 0, 0, #items * itemSize.Y + (#items - 1) * itemsPaddingOffset)
elseif scrollingDirection == Enum.ScrollingDirection.X then
canvasSize = UDim2.new(0, #items * itemSize.X + (#items - 1) * itemsPaddingOffset, 1, 0)
end
return Roact.createElement("ScrollingFrame", {
Size = UDim2.new(1, 0, 1, 0),
ScrollingEnabled = false, --Don't let the default select logic affect canvas position
CanvasSize = canvasSize,
Selectable = scrollingFrameProps.selectable or false,
ScrollBarThickness = 0,
ClipsDescendants = scrollingFrameProps.clipsDescendants,
BackgroundTransparency = 1,
ScrollingDirection = scrollingDirection,
[Roact.Ref] = self.scrollingFrameRef,
[Roact.Change.CanvasPosition] = function() self:updateViewBounds() end,
[Roact.Change.AbsoluteSize] = function() self:updateViewBounds() end,
}, children)
end
function WindowedScrollingFrame:didMount()
self:updateViewBounds()
end
function WindowedScrollingFrame:didUpdate(prevProps, prevState)
if not prevProps.inFocus and self.props.inFocus then
self.conn = GuiService:GetPropertyChangedSignal("SelectedCoreObject"):connect(function()
self:onSelectionChanged(GuiService.SelectedCoreObject)
end)
elseif prevProps.inFocus and not self.props.inFocus then
Utility.DisconnectEvent(self.conn)
end
if self.props ~= prevProps then
self:updateViewBounds()
end
end
return WindowedScrollingFrame
@@ -0,0 +1,57 @@
--[[
A simple component that allows you to bind to ContextActionService at CoreScript level
Props
name - the name of the binded action
callback - the function that is invoked
binds - the input that triggers the action
this is a table - example { Enum.KeyCode.ButtonA, Enum.KeyCode.ButtonX }
actionPriority - the action priority
Usage:
ContextActionCn = Roact.createElement(ContextActionEvent, {
name = "MyContextActionBind",
callback = function() print("context event") end,
binds = { Enum.KeyCode.Thumbstick2, Enum.KeyCode.ButtonB, Enum.KeyCode.A },
actionPriority = 1,
}),
Note: Cannot currently write a unit test for this component because it uses functions that
are RobloxScript security. LuaCore team is looking into a solution for this
]]
local ContextActionService = game:GetService("ContextActionService")
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local ContextActionEvent = Roact.Component:extend("ContextActionEvent")
local DEFAULT_ACTION_PRIORITY = 2000
function ContextActionEvent:render()
return nil
end
function ContextActionEvent:didMount()
if self.props.actionPriority then
ContextActionService:BindCoreActionAtPriority(self.props.name, self.props.callback, false, DEFAULT_ACTION_PRIORITY + self.props.actionPriority, unpack(self.props.binds))
else
ContextActionService:BindCoreAction(self.props.name, self.props.callback, false, unpack(self.props.binds))
end
end
function ContextActionEvent:didUpdate(oldProps)
if self.props.callback ~= oldProps.callback or self.props.name ~= oldProps.name then
ContextActionService:UnbindCoreAction(oldProps.name)
if self.props.actionPriority then
ContextActionService:BindCoreActionAtPriority(self.props.name, self.props.callback, false, DEFAULT_ACTION_PRIORITY + self.props.actionPriority, unpack(self.props.binds))
else
ContextActionService:BindCoreAction(self.props.name, self.props.callback, false, unpack(self.props.binds))
end
end
end
function ContextActionEvent:willUnmount()
ContextActionService:UnbindCoreAction(self.props.name)
end
return ContextActionEvent
@@ -0,0 +1,27 @@
--[[
Creates a component with a button image and hint text
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
return Roact.createElement("ImageLabel", {
Size = UDim2.new(0, 65, 0, 65),
Position = props.Position,
BackgroundTransparency = 1,
Image = props.Image,
}, {
Text = Roact.createElement("TextLabel", {
Size = UDim2.new(0, 0, 1, 0),
Position = UDim2.new(1, 5, 0, -3),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.ButtonSize,
TextColor3 = GlobalSettings.WhiteTextColor,
TextXAlignment = Enum.TextXAlignment.Left,
Text = props.Text,
})
})
end
@@ -0,0 +1,37 @@
--[[
Creates a component with a gamepad image with a resize hint for the overscan screen
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local Strings = require(Modules.Shell.LocalizedStrings)
return function(props)
return Roact.createElement("ImageLabel", {
Size = UDim2.new(0, 599, 0, 404),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/Controller@1080.png",
}, {
Line = Roact.createElement("Frame", {
Size = UDim2.new(0, 240, 0, 1),
Position = UDim2.new(0, 437, 0, 220),
BackgroundTransparency = 0,
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
}, {
InputHint = Roact.createElement("TextLabel", {
Size = UDim2.new(0, 0, 0, 0),
Position = UDim2.new(1, 3, 0, -1),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.ButtonSize,
TextColor3 = GlobalSettings.WhiteTextColor,
TextXAlignment = Enum.TextXAlignment.Left,
Text = Strings:LocalizedString("ResizeScreenInputHint"),
})
}),
})
end
@@ -0,0 +1,230 @@
local GameOptionsSettings = settings():FindFirstChild("Game Options")
local UserInputService = game:GetService("UserInputService")
local PlatformService = nil
pcall(function() PlatformService = game:GetService("PlatformService") end)
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local Utility = require(Modules.Shell.Utility)
local ContextActionEvent = require(Modules.Shell.Components.ContextActionEvent)
local ExternalEventConnection = require(Modules.Common.RoactUtilities.ExternalEventConnection)
local RenderStep = require(Modules.Shell.Components.RenderStep)
local MIN_EDGE_PERCENT = Vector2.new(0.85, 0.85)
local START_EDGE_PERCENT = Vector2.new(0.9, 0.9)
local CONSOLE_RESOLUTION = Vector2.new(1920, 1080)
local ZERO_VEC2 = Vector2.new(0,0)
local MAX_STICK_ACCELERATION = 3
local ACCELERATION_RATE = 1
local DPAD_STEP_AMOUNT = 2
local DPAD_CODE_TO_EDGE_PUSH = {
[Enum.KeyCode.DPadDown] = Vector2.new(0, DPAD_STEP_AMOUNT);
[Enum.KeyCode.DPadUp] = Vector2.new(0, -DPAD_STEP_AMOUNT);
[Enum.KeyCode.DPadLeft] = Vector2.new(-DPAD_STEP_AMOUNT, 0);
[Enum.KeyCode.DPadRight] = Vector2.new(DPAD_STEP_AMOUNT, 0);
}
local function EdgeImage(props)
return Roact.createElement("ImageLabel", {
Size = UDim2.new(0, 95, 0, 95),
Position = props.Position,
AnchorPoint = props.AnchorPoint,
BackgroundTransparency = 1,
Rotation = props.Rotation,
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/ScreenAdjustmentArrow.png",
})
end
local Edges = Roact.Component:extend("Edges")
function Edges:init()
local function getCurrentEdgePercent(newEdgePercent)
return Utility.ClampVector2(MIN_EDGE_PERCENT, Vector2.new(1, 1), newEdgePercent)
end
local function getCurrentEdgeSize(edgePercent)
local absoluteEdgeSize = edgePercent * CONSOLE_RESOLUTION
local roundedAbsoluteEdgeSize = Vector2.new(Utility.Round(absoluteEdgeSize.X/2), Utility.Round(absoluteEdgeSize.Y/2)) * 2
return Utility.ClampVector2(ZERO_VEC2, CONSOLE_RESOLUTION, roundedAbsoluteEdgeSize)
end
self.onAdjustThumbstick = function(actionName, inputState, inputObject)
self._stickPosition = Utility.GamepadLinearToCurve(Vector2.new(inputObject.Position.X, -inputObject.Position.Y), 0.2)
end
self.onAdjustDPad = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
local pushAmount = DPAD_CODE_TO_EDGE_PUSH[inputObject.KeyCode]
if pushAmount then
pushAmount = pushAmount / CONSOLE_RESOLUTION
if Utility.IsFinite(pushAmount.X) and Utility.IsFinite(pushAmount.Y) then
self._edgePercent = getCurrentEdgePercent(self._edgePercent + pushAmount)
self:setState({
currentSize = getCurrentEdgeSize(self._edgePercent)
})
end
end
end
end
self.onReset = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
self._stickPosition = ZERO_VEC2
self._edgePercent = getCurrentEdgePercent(START_EDGE_PERCENT)
self._acceleration = 1
self:setState({
currentSize = getCurrentEdgeSize(self._edgePercent),
})
end
end
self.onAccept = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
self._seenAPressed = true
elseif inputState == Enum.UserInputState.End and self._seenAPressed then
local success, err = pcall(function()
GameOptionsSettings.OverscanPX = math.min(1, self._edgePercent.X)
GameOptionsSettings.OverscanPY = math.min(1, self._edgePercent.Y)
end)
if self.props.onSetEdges then
self.props.onSetEdges()
end
end
end
self.onRenderStep = function()
local now = tick()
if self._lastUpdate then
if self._stickPosition ~= ZERO_VEC2 then
local delta = now - self._lastUpdate
local transformedStick = (self._stickPosition) * self._acceleration * delta * 0.05
self._edgePercent = getCurrentEdgePercent(transformedStick + self._edgePercent)
self._acceleration = math.min(self._acceleration + delta * ACCELERATION_RATE, MAX_STICK_ACCELERATION)
self:setState({
currentSize = getCurrentEdgeSize(self._edgePercent)
})
else
self._acceleration = 1
end
end
self._lastUpdate = now
end
self.onSuspended = function()
pcall(function()
GameOptionsSettings.OverscanPX = self._lastSavedOverscan.X
GameOptionsSettings.OverscanPY = self._lastSavedOverscan.Y
end)
end
local overscansetting = getCurrentEdgePercent(START_EDGE_PERCENT)
local startSize = getCurrentEdgeSize(overscansetting)
if UserInputService:GetPlatform() == Enum.Platform.XBoxOne then
pcall(function()
if GameOptionsSettings.OverscanPX > 0 and GameOptionsSettings.OverscanPY > 0 then
overscansetting = Vector2.new(GameOptionsSettings.OverscanPX, GameOptionsSettings.OverscanPY)
overscansetting = getCurrentEdgePercent(overscansetting)
startSize = getCurrentEdgeSize(overscansetting)
-- set the overscan settings to max so the user can accurately estimate their TVs overscan
-- save previous settings so we can save on suspend
self._lastSavedOverscan = Vector2.new(GameOptionsSettings.OverscanPX, GameOptionsSettings.OverscanPY)
GameOptionsSettings.OverscanPX = 1
GameOptionsSettings.OverscanPY = 1
end
end)
end
self._stickPosition = ZERO_VEC2
self._edgePercent = overscansetting
self._lastUpdate = nil
self._acceleration = 1
self._seenAPressed = false
self.state = {
currentSize = startSize,
}
end
function Edges:render()
return Roact.createElement("Frame", {
Size = UDim2.new(0, self.state.currentSize.X, 0, self.state.currentSize.Y),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
}, {
SelectionImage = Roact.createElement("ImageLabel", {
Size = UDim2.new(1, 2, 1, 2),
Position = UDim2.new(0, -1, 0, -1),
BackgroundTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(21, 21, 41, 41),
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/ScreenRangeOverlay.png",
}),
TopLeft = Roact.createElement(EdgeImage, {
Rotation = 0,
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
TopRight = Roact.createElement(EdgeImage, {
Rotation = 90,
Position = UDim2.new(1, 0, 0, 0),
AnchorPoint = Vector2.new(1, 0),
}),
BottomRight = Roact.createElement(EdgeImage, {
Rotation = 180,
Position = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(1, 1),
}),
BottomLeft = Roact.createElement(EdgeImage, {
Rotation = 270,
Position = UDim2.new(0, 0, 1, 0),
AnchorPoint = Vector2.new(0, 1),
}),
Render = Roact.createElement(RenderStep, {
name = "UpdateAdjustmentScreen",
priority = Enum.RenderPriority.Input.Value,
callback = self.onRenderStep,
}),
AdjustConnectorThumbstick = Roact.createElement(ContextActionEvent, {
name = "ThumbstickAdjustmentScreen",
callback = self.onAdjustThumbstick,
binds = { Enum.KeyCode.Thumbstick2 },
}),
AdjustConnectorDPad = Roact.createElement(ContextActionEvent, {
name = "DPadAdjustmentScreen",
callback = self.onAdjustDPad,
binds = { Enum.KeyCode.DPadDown, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight },
}),
ResetConnector = Roact.createElement(ContextActionEvent, {
name = "ResetAdjustmentScreen",
callback = self.onReset,
binds = { Enum.KeyCode.ButtonX },
}),
AcceptConnector = Roact.createElement(ContextActionEvent, {
name = "AcceptAdjustmentScreen",
callback = self.onAccept,
binds = { Enum.KeyCode.ButtonA },
}),
SuspendedCn = PlatformService and Roact.createElement(ExternalEventConnection, {
event = PlatformService.Suspended,
callback = self.onSuspended,
}),
})
end
return Edges
@@ -0,0 +1,80 @@
local TextService = game:GetService('TextService')
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local Strings = require(Modules.Shell.LocalizedStrings)
local Utility = require(Modules.Shell.Utility)
local ControllerHint = require(script.Parent.ControllerHint)
local Edges = require(script.Parent.Edges)
local ButtonHint = require(script.Parent.ButtonHint)
local Overscan = Roact.Component:extend("Overscan")
function Overscan:render()
-- We should really have a better API to handle text fits
local resetOffset = TextService:GetTextSize(
Strings:LocalizedString('ResetWord'),
Utility.ConvertFontSizeEnumToInt(GlobalSettings.ButtonSize),
GlobalSettings.RegularFont,
Vector2.new(0, 0)
)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BorderSizePixel = 1,
BackgroundTransparency = self.props.BackgroundTransparency,
BackgroundColor3 = Color3.new(3/255, 3/255, 3/255),
}, {
Roact.createElement("ImageLabel", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
BackgroundColor3 = Color3.new(3/255, 3/255, 3/255),
BorderSizePixel = 1,
Visible = self.props.ImageVisible,
Image = "rbxasset://textures/ui/Shell/ScreenAdjustment/Background.png",
}),
Title = Roact.createElement("TextLabel", {
Position = UDim2.new(0, 230, 0, 205),
BackgroundTransparency = 1,
Font = GlobalSettings.LightFont,
FontSize = GlobalSettings.HeaderSize,
TextColor3 = GlobalSettings.WhiteTextColor,
TextXAlignment = Enum.TextXAlignment.Left,
Text = Strings:LocalizedString("ScreenSizeWord");
}),
Prompt = Roact.createElement("TextLabel", {
Position = UDim2.new(0, 230, 0, 243),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.ButtonSize,
TextColor3 = GlobalSettings.WhiteTextColor,
TextXAlignment = Enum.TextXAlignment.Left,
Text = Strings:LocalizedString("ResizeScreenPrompt"),
}),
Controller = Roact.createElement(ControllerHint),
AcceptHint = Roact.createElement(ButtonHint, {
Position = UDim2.new(0.5, 25, 0.75, 0),
Image = "rbxasset://textures/ui/Shell/ButtonIcons/AButton.png",
Text = Strings:LocalizedString('AcceptWord'),
}),
ResetHint = Roact.createElement(ButtonHint, {
Position = UDim2.new(0.5, -25 - 65 - resetOffset.x, 0.75, 0),
Image = "rbxasset://textures/ui/Shell/ButtonIcons/XButton.png",
Text = Strings:LocalizedString('ResetWord'),
}),
EdgeSelector = Roact.createElement(Edges, {
onSetEdges = self.props.onUnmount,
}),
})
end
return Overscan
@@ -0,0 +1,43 @@
--[[
A simple component that allows you to bind to RenderStep
Props:
name - the name of the bind
priority - when during the render step to call the function
callback - function that will be invoked on render step
Usage:
RenderCn = Roact.createElement(RenderStep, {
name = "MyRenderStep",
priority = Enum.RenderPriority.Input.Value,
callback = function() print("stepping") end
})
]]
local RunService = game:GetService("RunService")
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local RenderStep = Roact.Component:extend("RenderStep")
function RenderStep:render()
return nil
end
function RenderStep:didMount()
RunService:BindToRenderStep(self.props.name, self.props.priority, self.props.callback)
end
function RenderStep:didUpdate(oldProps)
if self.props.callback ~= oldProps.callback or self.props.name ~= oldProps.name then
RunService:UnbindFromRenderStep(oldProps.name)
RunService:BindToRenderStep(self.props.name, self.props.priority, self.props.callback)
end
end
function RenderStep:willUnmount()
RunService:UnbindFromRenderStep(self.props.name)
end
return RenderStep
@@ -0,0 +1,14 @@
return function()
local Roact = require(game:GetService("CoreGui").RobloxGui.Modules.Common.Roact)
local RenderStep = require(script.Parent.RenderStep)
it("should create and destroy", function()
local element = Roact.createElement(RenderStep, {
name = "myRenderStep",
priority = Enum.RenderPriority.Input.Value,
callback = function() print("hello render step") end,
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,74 @@
--[[
This helper module will be a bridge to help migrate the console AppShell to using Roact.
The idea is to add the interface of a "Screen" to the Roact component, so the ScreenManager
can be happy and correctly manage screens that are not using Roact.
You will only need to wrap Roact components with this if they are the root of a route. Anything
that is a child screen/compoent of this should use Roact Routing.
Usage:
local MyScreen = require(ShellModules.Components.MyScreen)
local myRoactScreen = RoactScreenManagerWrapper.new(MyScreen, GuiRoot, {
backgroundTransparency = 0,
name = "MyScreen",
})
ScreenManager:OpenScreen(myRoactScreen)
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local ScreenManager = require(Modules.Shell.ScreenManager)
local AppState = require(Modules.Shell.AppState)
local RoactRodux = require(Modules.Common.RoactRodux)
local RoactScreenManagerWrapper = {}
RoactScreenManagerWrapper.__index = RoactScreenManagerWrapper
function RoactScreenManagerWrapper.new(roactComponent, parent, props, closeCallback)
local self = {}
props = props or {}
-- this will be passed to top level Roact components in order to close the screen
-- since we need some way to route back to the previous screen through the screen manager
local function onUnmount()
if self == ScreenManager:GetTopScreen() then
ScreenManager:CloseCurrent()
if closeCallback then
closeCallback()
end
end
end
props.onUnmount = onUnmount
local element = Roact.createElement(RoactRodux.StoreProvider, {
store = AppState.store,
}, {
Roact.createElement(roactComponent, props)
})
self._instance = nil
function self:Show()
self._instance = Roact.mount(element, parent, tostring(roactComponent))
end
function self:Hide()
self:Destruct()
end
function self:Focus()
-- do nothing
end
function self:RemoveFocus()
-- do nothing
end
setmetatable(self, RoactScreenManagerWrapper)
return self
end
function RoactScreenManagerWrapper:Destruct()
Roact.unmount(self._instance)
end
return RoactScreenManagerWrapper
@@ -0,0 +1,240 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local HttpService = game:GetService("HttpService")
local PlatformService = nil
pcall(function() PlatformService = game:GetService("PlatformService") end)
local Roact = require(Modules.Common.Roact)
local Components = Modules.Shell.Components
local PresenceCard = require(Components.Social.PresenceCard)
local WindowedScrollingFrame = require(Components.Common.WindowedScrollingFrame)
local Immutable = require(Modules.Common.Immutable)
local Spinner = require(Components.Common.Spinner)
local SideBarComponent = require(Components.Common.SideBar)
local Utility = require(Modules.Shell.Utility)
local EventHub = require(Modules.Shell.EventHub)
local GameJoinModule = require(Modules.Shell.GameJoin)
local Strings = require(Modules.Shell.LocalizedStrings)
local SoundManager = require(Modules.Shell.SoundManager)
local RedirectComponent = require(Modules.Shell.Components.Common.RedirectComponent)
local UserThumbnailLoader = require(Components.Common.UserThumbnailLoader)
local SIDE_BAR_ITEMS = {
JoinGame = Strings:LocalizedString("JoinGameWord"),
ViewDetails = Strings:LocalizedString("ViewGameDetailsWord"),
ViewProfile = Strings:LocalizedString("ViewGamerCardWord"),
EmptyFriendSideBar = Strings:LocalizedString("EmptyFriendSideBarWord"),
}
local FriendsScrollingView = Roact.PureComponent:extend("FriendsScrollingView")
function FriendsScrollingView:init()
self.state = {
sideBarInFocus = false,
sideBarShow = false,
currentSelectedIndex = 1,
}
self.onSideBarClose = function()
self:setState({
sideBarInFocus = false,
sideBarShow = false,
})
end
self.onSideBarOpen = function(data)
Utility.SetSelectedCoreObject(nil)
self:setState({
sideBarInFocus = true,
sideBarShow = true,
})
end
self.groupKey = HttpService:GenerateGUID(false)
end
function FriendsScrollingView:render()
local props = self.props
local friendsData = props.friendsData
local initialized = props.initialized
local actionPriority = self.props.actionPriority or 0
local hide = props.hide
local inFocus = props.inFocus
local sideBarInFocus = false
local friendsScrollingFrameInFocus = false
local children = {}
if not hide and inFocus then
sideBarInFocus = self.state.sideBarShow and self.state.sideBarInFocus
if not sideBarInFocus and #friendsData > 0 then
friendsScrollingFrameInFocus = true
end
end
if initialized and friendsData then
if #friendsData > 0 then
local itemSize = Vector2.new(440, 120)
local itemsPaddingOffset = 20
local itemTotalSizeY = itemSize.Y + itemsPaddingOffset
local itemsCount = math.floor(self.props.size.Y.Offset / itemTotalSizeY)
assert(itemsCount ~= 0, "The scrolling window is too small to accommodate any presence card.")
--We should have at least two items to ensure we can always select the top second item
--while keep the top first item fully in view.
--If the window is too small, we will always select the top first item.
local itemOffsetStart = itemsCount > 2 and itemTotalSizeY or 0
--This make sure when we scroll down the top card won't be clipped
local itemOffsetEnd = self.props.size.Y.Offset - itemsCount * itemTotalSizeY + itemsPaddingOffset
children.FriendsScrollingFrame = Roact.createElement(WindowedScrollingFrame, {
items = friendsData,
itemSize = itemSize,
itemsPaddingOffset = itemsPaddingOffset,
itemOffsetStart = itemOffsetStart,
itemOffsetEnd = itemOffsetEnd,
scrollingDirection = Enum.ScrollingDirection.Y,
inFocus = friendsScrollingFrameInFocus,
renderItem = function(data, index)
local presenceCardProps = Immutable.JoinDictionaries(data, {
layoutOrder = index,
size = UDim2.new(0, itemSize.X, 0, itemSize.Y),
focused = inFocus and self.state.currentSelectedIndex == index,
selected = friendsScrollingFrameInFocus and self.state.currentSelectedIndex == index,
})
--Set up callbacks
presenceCardProps.onActivated = function(bt)
SoundManager:Play("SideMenuSlideIn")
self.onSideBarOpen()
end
presenceCardProps.onSelectionGained = function()
if self.state.currentSelectedIndex ~= index then
self:setState({
currentSelectedIndex = index,
})
end
end
return Roact.createElement(PresenceCard, presenceCardProps)
end
})
local data = friendsData[self.state.currentSelectedIndex]
if inFocus and data and data.robloxuid then
children.ProfileImage = Roact.createElement(UserThumbnailLoader, {
rbxuid = data.robloxuid,
thumbnailType = Enum.ThumbnailType.AvatarThumbnail,
thumbnailSize = Enum.ThumbnailSize.Size352x352,
position = UDim2.new(1, 101, 0, 0),
size = UDim2.new(0, 680, 0, 680),
backgroundTransparency = 1,
showSpinner = true
})
end
if self.state.sideBarShow and data then
local sideBarButtons = {}
if data.robloxuid and data.robloxuid > 0 and data.robloxStatus == "InGame" then
local placeId = data.placeId
local lastLocation = data.lastLocation
local robloxuid = data.robloxuid
table.insert(sideBarButtons, {
text = SIDE_BAR_ITEMS.JoinGame,
callback = function()
GameJoinModule:StartGame(GameJoinModule.JoinType.Follow, robloxuid)
end
})
table.insert(sideBarButtons, {
text = SIDE_BAR_ITEMS.ViewDetails,
callback = function()
EventHub:dispatchEvent(EventHub.Notifications["OpenGameDetail"], placeId, lastLocation, nil)
end
})
end
if data.xuid and #data.xuid > 0 and PlatformService then
local xuid = data.xuid
table.insert(sideBarButtons, {
text = SIDE_BAR_ITEMS.ViewProfile,
callback = function()
-- NOTE: This will try to pop up the xbox system gamer card, failure will be handled by the xbox.
pcall(function()
PlatformService:PopupProfileUI(Enum.UserInputType.Gamepad1, xuid)
end)
end
})
end
local sideBarText = nil
if #sideBarButtons == 0 then
sideBarText = SIDE_BAR_ITEMS.EmptyFriendSideBar
sideBarButtons = nil
end
children.SideBar = Roact.createElement(SideBarComponent, {
actionPriority = actionPriority + 1,
text = sideBarText,
buttons = sideBarButtons,
inFocus = sideBarInFocus,
onClose = self.onSideBarClose,
onRemoveFocus = function()
self:setState({
sideBarInFocus = false
})
end,
})
end
else
children.NoFriendsView = props.noFriendsView
end
else
children.Spinner = Roact.createElement(Spinner)
end
children.NavObj = Roact.createElement(RedirectComponent, {
ActionPriority = actionPriority,
Key = self.groupKey,
InFocus = inFocus,
RedirectBack = props.redirectBack,
RedirectLeft = props.redirectLeft,
RedirectRight = props.redirectRight,
RedirectUp = props.redirectUp,
RedirectDown = props.redirectDown,
})
return Roact.createElement("Frame", {
BackgroundTransparency = 1,
Size = self.props.size,
Position = self.props.position,
[Roact.Ref] = function(rbx)
self.ref = rbx
end,
Visible = not hide,
}, children)
end
function FriendsScrollingView:didMount()
delay(0, function()
if self.props.hide == false and self.props.inFocus then
if self.ref ~= nil then
Utility.RemoveSelectionGroup(self.groupKey)
Utility.AddSelectionParent(self.groupKey, self.ref)
end
end
end)
end
function FriendsScrollingView:didUpdate(previousProps, previousState)
if self.props.hide or self.props.inFocus == previousProps.inFocus then
return
end
if self.props.inFocus then
if self.ref then
Utility.RemoveSelectionGroup(self.groupKey)
Utility.AddSelectionParent(self.groupKey, self.ref)
end
else
Utility.RemoveSelectionGroup(self.groupKey)
end
end
function FriendsScrollingView:willUnmount()
Utility.RemoveSelectionGroup(self.groupKey)
end
return FriendsScrollingView
@@ -0,0 +1,158 @@
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local memoize = require(Modules.Common.memoize)
local RoactRodux = require(Modules.Common.RoactRodux)
local CategoryMenuView = require(Modules.Shell.Components.Common.CategoryMenuView)
local SplitViewLR = require(Modules.Shell.Components.Common.SplitViewLR)
local Utility = require(Modules.Shell.Utility)
local SoundManager = require(Modules.Shell.SoundManager)
local Strings = require(Modules.Shell.LocalizedStrings)
local FriendsScrollingView = require(Modules.Shell.Components.Social.FriendsScrollingView)
local NoFriendsView = require(Modules.Shell.Components.Social.NoFriendsView)
local PageKeys = require(Modules.Shell.PageKeys)
local FriendsView = Roact.PureComponent:extend("FriendsView")
local MenuKey = PageKeys.FriendCategories.Key
local FriendCategoriesKeys = {
OnlineFriends = PageKeys.FriendCategories.OnlineFriends.Key,
AllFriends = PageKeys.FriendCategories.AllFriends.Key,
}
local FriendCategories = {}
FriendCategories[FriendCategoriesKeys.OnlineFriends] = { Order = 1, StringKey = "OnlineWord" }
FriendCategories[FriendCategoriesKeys.AllFriends] = { Order = 2, StringKey = "AllWord" }
local function onSelectSection(self, key)
if self.state.currentPage ~= key then
self:setState({
currentPage = key,
selectedPage = MenuKey,
})
end
end
function FriendsView:init()
self.selectedPage = MenuKey
self.onSelectionGained = function(key)
onSelectSection(self,key)
end
self.state = {
currentPage = FriendCategoriesKeys.OnlineFriends,
selectedPage = MenuKey,
}
self.enterSection = function()
SoundManager:Play("OverlayOpen")
Utility.SetSelectedCoreObject(nil)
self:setState({
selectedPage = self.state.currentPage,
})
end
self.exitSection = function()
SoundManager:Play("PopUp")
self:setState({
selectedPage = MenuKey,
})
end
end
function FriendsView:render()
local actionPriority = self.props.actionPriority or 0
local currentPage = self.state.currentPage
local friendsViewInFocus = self.props.inFocus
local friendsViewHide = self.props.hide
local friendCategoriesMenuInFocus = false
local friendPagesInFocus = false
if not friendsViewHide and friendsViewInFocus then
if self.state.selectedPage == MenuKey then
friendCategoriesMenuInFocus = true
end
if self.state.selectedPage ~= MenuKey then
friendPagesInFocus = true
end
end
local enterSection = self.enterSection
if currentPage == PageKeys.FriendCategories.OnlineFriends.Key and #self.props.onlineFriendsData == 0 then
enterSection = nil
elseif currentPage == PageKeys.FriendCategories.AllFriends.Key and #self.props.allFriendsData == 0 then
enterSection = nil
end
return Roact.createElement("Frame", {
Size = UDim2.new(1, -58, 1, 0),
Position = UDim2.new(0, 58, 0, 0),
BackgroundTransparency = 1,
},{
Mainview = Roact.createElement(SplitViewLR, {
Bias = 0.265,
LeftView = Roact.createElement(CategoryMenuView, {
Key = MenuKey,
Categories = FriendCategories,
InFocus = friendCategoriesMenuInFocus,
DefaultCategoryFocus = FriendCategoriesKeys.OnlineFriends,
OnSelectSection = self.onSelectionGained,
EnterSection = enterSection,
RedirectUp = self.props.redirectUp,
ActionPriority = actionPriority,
}),
RightView = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
},{
OnlineFriendsView = Roact.createElement(FriendsScrollingView, {
friendsData = self.props.onlineFriendsData,
initialized = self.props.initialized,
hide = currentPage ~= PageKeys.FriendCategories.OnlineFriends.Key,
inFocus = friendPagesInFocus,
redirectLeft = self.exitSection,
redirectBack = self.exitSection,
redirectUp = self.props.redirectUp,
redirectRight = self.props.redirectRight,
size = UDim2.new(0, 440, 0, 770),
noFriendsView = Roact.createElement(NoFriendsView, {
text = Strings:LocalizedString("NoFriendsOnlinePhrase"),
}),
actionPriority = actionPriority + 1,
}),
AllFriendsView = Roact.createElement(FriendsScrollingView, {
friendsData = self.props.allFriendsData,
initialized = self.props.initialized,
hide = currentPage ~= PageKeys.FriendCategories.AllFriends.Key,
inFocus = friendPagesInFocus,
redirectLeft = self.exitSection,
redirectBack = self.exitSection,
redirectUp = self.props.redirectUp,
redirectRight = self.props.redirectRight,
size = UDim2.new(0, 440, 0, 770),
noFriendsView = Roact.createElement(NoFriendsView, {
text = Strings:LocalizedString("PlayAndMakeFriendsPhrase"),
}),
actionPriority = actionPriority + 1,
}),
}),
}),
})
end
local filterOnlineFriends = memoize(function(friendsData)
local onlineFriendsData = {}
for _, data in ipairs(friendsData) do
if data.robloxStatus ~= "Offline" or data.xboxStatus == "Online" then
table.insert(onlineFriendsData, data)
end
end
return onlineFriendsData
end)
local function mapStateToProps(state, props)
local friendsData = state.RenderedFriends.data
return {
allFriendsData = friendsData,
onlineFriendsData = filterOnlineFriends(friendsData),
initialized = state.RenderedFriends.initialized
}
end
return RoactRodux.UNSTABLE_connect2(mapStateToProps)(FriendsView)
@@ -0,0 +1,32 @@
--[[
Creates a component for no friends
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
return function(props)
return Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1
}, {
NoFriendsIcon = Roact.createElement("ImageLabel", {
Size = UDim2.new(0, 296, 0, 259),
Position = UDim2.new(0.5, -148, 0, 100),
BackgroundTransparency = 1,
Image = "rbxasset://textures/ui/Shell/Icons/FriendsIcon@1080.png",
}),
NoFriendsText = Roact.createElement("TextLabel", {
Size = UDim2.new(0, 440, 0, 72),
Position = UDim2.new(0.5, -220, 0, 392),
BackgroundTransparency = 1,
Font = GlobalSettings.RegularFont,
FontSize = GlobalSettings.ButtonSize,
TextColor3 = GlobalSettings.WhiteTextColor,
Text = props.text,
TextYAlignment = Enum.TextYAlignment.Top,
TextWrapped = true
}),
})
end
@@ -0,0 +1,223 @@
--[[
Creates a PresenceCard component
Props:
gamertag: string - Xbox Gamertag.
robloxName: string - Roblox name.
robloxuid: int - Roblox user id.
xuid: int - Xbox user id.
robloxStatus: string - User's roblox xboxStatus.
xboxStatus: string - User's xbox xboxStatus.
lastLocation: string - User's last location info.
layoutOrder: int - Controls the sorting priority of this button.
size: UDim2 - The size of the presence card.
onSelectionGained : function(guiObject : Ref<GuiObject>) -
Fires when the GuiObject is being focused on with the Gamepad selector.
onSelectionLost : function(guiObject : Ref<GuiObject>) -
Fires when the Gamepad selector stops focusing on the GuiObject.
onActivated : function(guiObject : Ref<GuiObject>) -
Fires when the button is activated.
]]
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Roact = require(Modules.Common.Roact)
local GlobalSettings = require(Modules.Shell.GlobalSettings)
local Components = Modules.Shell.Components
local UserThumbnailLoader = require(Components.Common.UserThumbnailLoader)
local Strings = require(Modules.Shell.LocalizedStrings)
local Utility = require(Modules.Shell.Utility)
local SoundComponent = require(Modules.Shell.Components.Common.SoundComponent)
local PresenceCard = Roact.PureComponent:extend("PresenceCard")
function PresenceCard:init()
self.selectionImageObject = Utility.Create "ImageLabel"({
Name = "SelectorImage",
Image = GlobalSettings.Images.ButtonSelector,
Position = UDim2.new(0, -7, 0, -7),
Size = UDim2.new(1, 14, 1, 14),
BackgroundTransparency = 1,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(31, 31, 63, 63),
})
self.onCreate = function(rbx)
self.ref = rbx
end
self.defaultProps = {
buttonColor3 = GlobalSettings.Colors.WhiteButton,
buttonTransparency = 0.8,
textColor3 = GlobalSettings.Colors.WhiteText,
iconColor3 = GlobalSettings.Colors.WhiteText,
}
self.focusedProps = {
buttonColor3 = GlobalSettings.Colors.BlueButton,
buttonTransparency = 0,
textColor3 = GlobalSettings.Colors.BlackText,
iconColor3 = GlobalSettings.Colors.BlackText,
}
self.buttonImage = GlobalSettings.Images.ButtonDefault
end
function PresenceCard:render()
local props = self.props
local focused = self.props.focused
local currProps = focused and self.focusedProps or self.defaultProps
local gamertagText = props.gamertag or ""
local robloxNameText = props.robloxName or ""
local showGamertag = gamertagText ~= ""
local showRobloxName = robloxNameText ~= ""
local statusText = ""
local statusImageColor3 = GlobalSettings.Colors.GreySelectedButton
local function setPresence(statusStr, statusColor)
statusImageColor3 = statusColor
if statusStr and statusStr ~= "" then
statusText = statusStr
end
end
if props.robloxStatus == "InGame" then
setPresence(props.lastLocation, GlobalSettings.Colors.GreenText)
elseif props.robloxStatus == "InStudio" then
setPresence(props.lastLocation, GlobalSettings.Colors.OrangeText)
elseif props.robloxStatus == "Online" then
setPresence("Roblox", GlobalSettings.Colors.BlueText)
else
if props.xboxStatus and props.xboxStatus == "Online" then
setPresence(Strings:LocalizedString("OnlineWord"), GlobalSettings.Colors.BlueText)
else
setPresence(Strings:LocalizedString("OfflineWord"), GlobalSettings.Colors.GreyText)
end
end
return Roact.createElement("ImageButton", {
Image = self.buttonImage,
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
LayoutOrder = props.layoutOrder,
Size = props.size or UDim2.new(0, 440, 0, 120),
ImageColor3 = currProps.buttonColor3,
ImageTransparency = currProps.buttonTransparency,
ScaleType = Enum.ScaleType.Slice,
SliceCenter = Rect.new(8, 8, 9, 9),
SelectionImageObject = self.selectionImageObject,
BackgroundTransparency = 1,
[Roact.Event.SelectionGained] = props.onSelectionGained,
[Roact.Event.SelectionLost] = props.onSelectionLost,
[Roact.Event.Activated] = props.onActivated,
[Roact.Ref] = self.onCreate,
},{
MoveSelection = Roact.createElement(SoundComponent, {
SoundName = "MoveSelection",
}),
AvatarImage = Roact.createElement(UserThumbnailLoader, {
rbxuid = props.robloxuid,
thumbnailType = Enum.ThumbnailType.HeadShot,
thumbnailSize = Enum.ThumbnailSize.Size100x100,
position = UDim2.new(0, 10, 0, 10),
size = UDim2.new(0, 100, 0, 100),
}),
ContentContainer = Roact.createElement("Frame",{
Size = UDim2.new(1, -140, 1, 0),
Position = UDim2.new(0, 126, 0, 0),
BackgroundTransparency = 1,
ClipsDescendants = true,
}, {
UIPadding = Roact.createElement("UIPadding", {
PaddingTop = UDim.new(0, 10),
PaddingBottom = UDim.new(0, 10)
}),
UIListLayout = Roact.createElement("UIListLayout", {
Padding = UDim.new(0, 7),
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
GamertagContainer = showGamertag and Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 30),
LayoutOrder = 1,
BackgroundTransparency = 1
},{
GamertagLabel = Roact.createElement("TextLabel", {
Text = gamertagText,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0, 0, 0, 0),
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = currProps.textColor3,
Font = GlobalSettings.Fonts.Regular,
TextSize = 30,
TextScaled = true,
BackgroundTransparency = 1,
}),
}),
RobloxNameContainer = showRobloxName and Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 30),
LayoutOrder = 2,
BackgroundTransparency = 1
},{
RobloxIcon = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Image = GlobalSettings.Images.RobloxIcon,
ImageColor3 = currProps.iconColor3,
Position = UDim2.new(0, 0, 0, 1),
Size = UDim2.new(0, 28, 0, 28),
}),
RobloxNameLabel = Roact.createElement("TextLabel", {
Text = robloxNameText,
Size = UDim2.new(1, -38, 1, 0),
Position = UDim2.new(0, 38, 0, 0),
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = currProps.textColor3,
Font = GlobalSettings.Fonts.Regular,
TextSize = 30,
TextScaled = true,
BackgroundTransparency = 1,
})
}),
StatusContainer = Roact.createElement("Frame", {
Size = UDim2.new(1, 0, 0, 26),
LayoutOrder = 3,
BackgroundTransparency = 1
},{
PresenceStatusImage = Roact.createElement("ImageLabel", {
BackgroundTransparency = 1,
Image = GlobalSettings.Images.OnlineStatusIcon,
Size = UDim2.new(0, 18, 0, 18),
Position = UDim2.new(0, 5, 0, 4),
ImageColor3 = statusImageColor3,
}),
PresenceLabel = Roact.createElement("TextLabel", {
Text = statusText,
Size = UDim2.new(1, -38, 1, 0),
Position = UDim2.new(0, 33, 0, 0),
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = currProps.textColor3,
Font = GlobalSettings.Fonts.Regular,
TextSize = 26,
BackgroundTransparency = 1,
})
}),
})
})
end
function PresenceCard:didMount()
delay(0, function()
if self.props.selected then
Utility.SetSelectedCoreObject(self.ref)
end
end)
end
function PresenceCard:didUpdate(previousProps, previousState)
if not previousProps.selected and self.props.selected then
Utility.SetSelectedCoreObject(self.ref)
end
end
return PresenceCard

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