-- 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