--[[ // PackageData.lua // Created by Kip Turner, Bo Zhang // Copyright Roblox 2017 ]] local CoreGui = game:GetService("CoreGui") local GuiRoot = CoreGui:FindFirstChild("RobloxGui") local Modules = GuiRoot:FindFirstChild("Modules") local ShellModules = Modules:FindFirstChild("Shell") local ContentProvider = game:GetService("ContentProvider") local MarketplaceService = game:GetService('MarketplaceService') local PlatformService = nil pcall(function()PlatformService = game:GetService('PlatformService') end) local ThirdPartyUserService = nil pcall(function()ThirdPartyUserService = game:GetService('ThirdPartyUserService') end) local Utility = require(ShellModules:FindFirstChild('Utility')) local Http = require(ShellModules:FindFirstChild('Http')) local UserData = require(ShellModules:FindFirstChild('UserData')) local EventHub = require(ShellModules:FindFirstChild('EventHub')) local GlobalSettings = require(ShellModules:FindFirstChild('GlobalSettings')) local ReloaderManager = require(ShellModules:FindFirstChild('ReloaderManager')) local CreateCacheData = require(ShellModules:FindFirstChild('CachedData')) local ThumbnailLoader = require(ShellModules:FindFirstChild('ThumbnailLoader')) local XboxAppState = require(ShellModules:FindFirstChild('AppState')) local RequestingWearAsset = false local function AwaitWearAssetRequest() while RequestingWearAsset do wait(0.1) end end local RequestingBuyAsset = false local function AwaitBuyAssetRequest() while RequestingBuyAsset do wait(0.1) end end local function PreloadCharacterAppearanceAsync() local character = nil local success = pcall(function() character = game.Players:GetCharacterAppearanceAsync(UserData:GetLocalUserIdAsync()) end) if character then ContentProvider:PreloadAsync({ character }) end return success end --Hard code the assetId to productId map local AvatarAssetId_XboxProductIdMap = { ['807301633'] = '899a379d-0a66-4b07-8bcd-29b1e38699ba'; --Boy Avatar ['807340263'] = '7dba5b02-02be-4442-814e-9b9ebf6d66bf'; --Girl Avatar } local function CreatePackageItem(data) local this = {} this.Owned = false this.Wearing = false this.OwnershipChanged = Utility.Signal() this.IsWearingChanged = Utility.Signal() local productInfo = nil local partIds = {} function this:GetAssetId() local assetId = data and data['AssetId'] if not assetId then assetId = data and data['Item'] and data['Item']['AssetId'] end return tonumber(assetId) end function this:OpenAvatarDetailInXboxStore() local assetId = self:GetAssetId() if PlatformService then local XboxProductId = AvatarAssetId_XboxProductIdMap[tostring(assetId)] if XboxProductId then PlatformService:OpenProductDetail(XboxProductId) end end end function this:IsXboxAddOn() return AvatarAssetId_XboxProductIdMap[tostring(self:GetAssetId())] and true or false end function this:GetProductIdAsync() while not productInfo do wait() end return productInfo and productInfo['ProductId'] end function this:GetPartIdsAsync() while not partIds do wait() end return partIds end function this:UpdatePartIdsAsync() local assetId = self:GetAssetId() if assetId then while not partIds do wait() end --Reset partIds partIds = nil local response = Http.GetPackageAssetsAsync(assetId) if response then partIds = response["assetIds"] end if partIds == nil then partIds = {} else table.sort(partIds) end end return partIds end function this:IsWearing() return self.Wearing end function this:SetWearing(newWearing) if newWearing ~= self.Wearing then self.Wearing = newWearing self.IsWearingChanged:fire(newWearing) end end function this:BuyAsync() EventHub:dispatchEvent(EventHub.Notifications["AvatarPurchaseBegin"], self:GetAssetId()) Utility.DebugLog("Do buy", 'productId', self:GetProductIdAsync(), 'robuxPrice', self:GetRobuxPrice()) AwaitBuyAssetRequest() RequestingBuyAsset = true local purchaseResult = Http.PurchaseProductAsync(self:GetProductIdAsync(), self:GetRobuxPrice(), self:GetCreatorId(), 1) RequestingBuyAsset = false local nowOwns = purchaseResult and purchaseResult['TransactionVerb'] == 'bought' if nowOwns then EventHub:dispatchEvent(EventHub.Notifications["AvatarPurchaseSuccess"], self:GetAssetId(), nowOwns) end return purchaseResult end function this:IsOwned() return self.Owned end function this:SetOwned(newOwned) if self.Owned ~= newOwned then self.Owned = newOwned self.OwnershipChanged:fire(newOwned) end end function this:GetRobuxPrice() local robuxPrice = data and data['PriceInRobux'] if not robuxPrice then robuxPrice = data and data['Product'] and data['Product']['PriceInRobux'] end local isPublicDomain = data and data['IsPublicDomain'] == true if isPublicDomain == nil then isPublicDomain = data and data['Product'] and data['Product']['IsPublicDomain'] == true end if not robuxPrice and isPublicDomain then robuxPrice = 0 end return robuxPrice end function this:GetCreatorId() return data and data['Creator'] and data['Creator']['Id'] end function this:WearAsync() local assetId = self:GetAssetId() if assetId then EventHub:dispatchEvent(EventHub.Notifications["AvatarEquipBegin"], assetId) AwaitWearAssetRequest() RequestingWearAsset = true local result; result = Http.SetWearingAssetsAsync(self:GetPartIdsAsync()) RequestingWearAsset = false if result and result['success'] == true then EventHub:dispatchEvent(EventHub.Notifications["AvatarEquipSuccess"], assetId) end return result end end function this:GetName() local resultPackageName = self:GetFullName() if resultPackageName then local colonPosition = string.find(resultPackageName, ":") if colonPosition then resultPackageName = string.sub(resultPackageName, 1, colonPosition - 1) end else resultPackageName = "Unknown" end return resultPackageName end function this:GetFullName() local name = data and data['Name'] if not name then name = data and data['Item'] and data["Item"]['Name'] end return name or "Unknown" end function this:GetDescriptionAsync() while not productInfo do wait() end return productInfo and productInfo['Description'] end spawn(function() productInfo = MarketplaceService:GetProductInfo(this:GetAssetId()) if productInfo == nil then productInfo = {} end end) --We get partIds when we create package this:UpdatePartIdsAsync() return this end local PackageData = {} do --Cached Data --InternalPackageDataCache only gets updated at intervals local InternalPackageDataCache = nil --VisiblePackageDataCache gets updated by user operation --and will be checked and updated when GetCachedData() is called local VisiblePackageDataCache = nil local ProfileImageThumbnailLoader = nil local UpdateFuncId = nil --Start Image Update Times for wearing local ProfileImageUpdateForWearing = 0 --Bool to check whether we update pane while updating caching data local visiblePackageUpdating = false local function ResetCacheData() InternalPackageDataCache = nil VisiblePackageDataCache = nil ProfileImageThumbnailLoader = nil UpdateFuncId = nil ProfileImageUpdateForWearing = 0 visiblePackageUpdating = false end local function deepcopy(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[deepcopy(orig_key)] = deepcopy(orig_value) end setmetatable(copy, deepcopy(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end local UserChangedCount = 0 --Add to cope with avatar purchase through Xbox Store local ConsumePurchasedConn = nil local function OnUserAccountChanged() local startCount = UserChangedCount ResetCacheData() spawn(function() if startCount == UserChangedCount then spawn(PreloadCharacterAppearanceAsync) local RefreshInterval = GlobalSettings.AvatarPaneRefreshInterval ReloaderManager:removeReloader("PackageData") UpdateFuncId = ReloaderManager:addReloaderFunc("PackageData", PackageData.UpdateCachedDataAsync, RefreshInterval) ReloaderManager:callReloaderFunc("PackageData", UpdateFuncId) Utility.DisconnectEvent(ConsumePurchasedConn) if PlatformService then ConsumePurchasedConn = PlatformService.ConsumePurchased:connect(function(platformPurchaseResult, purchasedConsumablesInfo) if platformPurchaseResult == 3 then spawn(function() PackageData:UpdatePurchasedConsumablesAsync(purchasedConsumablesInfo) end) end end) end end end) end EventHub:addEventListener(EventHub.Notifications["AuthenticationSuccess"], "PackageData", OnUserAccountChanged) local function OnUserSignOut() UserChangedCount = UserChangedCount + 1 ReloaderManager:removeReloader("PackageData") EventHub:removeEventListener(EventHub.Notifications["AvatarEquipBegin"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseBegin"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["AvatarEquipSuccess"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["CharacterUpdated"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["CharacterEquipped"], "VisiblePackageDataCache") Utility.DisconnectEvent(ConsumePurchasedConn) end if ThirdPartyUserService then ThirdPartyUserService.ActiveUserSignedOut:connect(OnUserSignOut) end local function GetAvailableXboxCatalogPackagesAsync() local isFinalPage = false local packages = {} local index = 0 local count = 100 repeat local result = nil Utility.ExponentialRepeat( function() return result == nil end, function()result = Http.GetXboxProductsAsync(index, count) end, 2) if result then local items = result['Products'] if items then if #items < count then isFinalPage = true end for _, itemInfo in pairs(items) do table.insert(packages, itemInfo) end end end index = index + count until result == nil or isFinalPage if isFinalPage then return packages end end local function GetOwnedCatalogPackageIdsByUserAsync(userId) local packages = Http.GetUserOwnedPackagesAsync(userId) if packages then local data = packages['IsValid'] and packages['Data'] local items = data and data['Items'] local result = {} if items then for _, itemInfo in pairs(items) do local assetId = itemInfo and itemInfo['Item'] and itemInfo['Item']['AssetId'] result[assetId] = itemInfo end end return result end end local function getCatalogPackagesAsync() local xboxCatalogPackages = GetAvailableXboxCatalogPackagesAsync() local myPackages = GetOwnedCatalogPackageIdsByUserAsync(XboxAppState.store:getState().RobloxUser.rbxuid) if xboxCatalogPackages and myPackages then local result = {} result.Packages = {} result.OwnedInfo = {} --We use array to store xboxCatalogPackages as the order is important --and then use result.OwnedInfo to store OwnedInfo to avoid duplicate with myPackages for _, packageInfo in pairs(xboxCatalogPackages) do local package = CreatePackageItem(packageInfo) local assetId = package:GetAssetId() local owned = (myPackages[assetId] ~= nil) result.OwnedInfo[assetId] = owned table.insert(result.Packages, package) end for assetId, packageInfo in pairs(myPackages) do if result.OwnedInfo[assetId] == nil then local package = CreatePackageItem(packageInfo) result.OwnedInfo[assetId] = true table.insert(result.Packages, package) end end return result end end --Get new packages and owned info function PackageData:GetPackagesAndOwnedInfoAsync() local startCount = UserChangedCount local result = nil Utility.ExponentialRepeat( function() return result == nil and startCount == UserChangedCount end, function()result = getCatalogPackagesAsync() end, 3) if startCount ~= UserChangedCount then result = nil end return result end --Utility function to compare arrays local function CompareAssetIdArrays(arr1, arr2) if type(arr1) == 'table' and type(arr2) == 'table' then if #arr1 == #arr2 then for i = 1, #arr1 do if tonumber(arr1[i]) ~= tonumber(arr2[i]) then return false end end return true end end return false end --Get WearingAssetId from packages function PackageData:GetWearingPackageAssetIdAsync(packages) local startCount = UserChangedCount local newWearingAssetId = nil local rbxuid = XboxAppState.store:getState().RobloxUser.rbxuid if rbxuid then if packages then local response = Http.GetCurrentlyWearingAsync(rbxuid) if not response then return end local wornAssetIds = response["assetIds"] for _, package in pairs(packages) do if CompareAssetIdArrays(package:GetPartIdsAsync(), wornAssetIds) then newWearingAssetId = tonumber(package:GetAssetId()) break end end end end if startCount ~= UserChangedCount then newWearingAssetId = nil end return newWearingAssetId end function PackageData:GetProfileImageAsync() local startCount = UserChangedCount local newProfileImage = {} if ProfileImageThumbnailLoader then ProfileImageThumbnailLoader:Cancel() end ProfileImageThumbnailLoader = ThumbnailLoader:Create(newProfileImage, XboxAppState.store:getState().RobloxUser.rbxuid, ThumbnailLoader.AvatarSizes.Size352x352, ThumbnailLoader.AssetType.Avatar, true) ProfileImageThumbnailLoader:LoadAsync(false, false) --If user has changed or we fail to get the ImageUrl, return nil if newProfileImage.Image == "" or startCount ~= UserChangedCount then newProfileImage = nil end return newProfileImage end --Called at intervals, try to fetch latest CachedData local debounceUpdateCachedData = false function PackageData.UpdateCachedDataAsync() if debounceUpdateCachedData then while debounceUpdateCachedData do wait() end end debounceUpdateCachedData = true local startCount = UserChangedCount local maxRetry = 2 local valid = false Utility.ExponentialRepeat( function() return valid == false and startCount == UserChangedCount end, function() valid = PackageData:UpdateCachedData() end, maxRetry) debounceUpdateCachedData = false end --Three Utility functions to update data local function UpdateDataProfileImage(data, newProfileImage) if not data then return end data.ProfileImage:Update(CreateCacheData(newProfileImage, tick())) end local function UpdateDataWearing(data, newWearingAssetId) if not data then return end local packages = data.Packages.Data data.WearingAssetId:Update(CreateCacheData(newWearingAssetId, tick())) for _, package in pairs(packages) do if package then if package:IsOwned() then package:SetWearing(newWearingAssetId == package:GetAssetId()) else package:SetWearing(false) end end end end local function UpdateDataOwned(data, newOwnedAssetId, newOwned) if not data then return end local packages = data.Packages.Data if data.OwnedInfo then if data.OwnedInfo[newOwnedAssetId] then data.OwnedInfo[newOwnedAssetId]:Update(CreateCacheData(newOwned, tick())) else data.OwnedInfo[newOwnedAssetId] = CreateCacheData(newOwned, tick()) end end for _, package in pairs(packages) do if package then if package:GetAssetId() == newOwnedAssetId then package:SetOwned(newOwned) if not newOwned then --We can't wear not owned packages package:SetWearing(false) end break end end end end function PackageData:UpdateCachedData() local validUpdate = false local startCount = UserChangedCount visiblePackageUpdating = false --Get Packages and OwnedInfo local newResult = PackageData:GetPackagesAndOwnedInfoAsync() local newPackages = newResult and newResult.Packages local newOwnedInfo = newResult and newResult.OwnedInfo --Must make sure we get valid Packages and OwnedInfo if newResult and newPackages and newOwnedInfo then local newPackageDataCache = {} newPackageDataCache.Packages = CreateCacheData(newPackages, tick()) newPackageDataCache.OwnedInfo = {} if newOwnedInfo then for assetId, owned in pairs(newOwnedInfo) do newPackageDataCache.OwnedInfo[assetId] = CreateCacheData(owned, tick()) end end --Get WearingAssetId local newWearingAssetId = PackageData:GetWearingPackageAssetIdAsync(newPackages) newPackageDataCache.WearingAssetId = CreateCacheData(newWearingAssetId, tick()) --Get ProfileImage local newProfileImage = PackageData:GetProfileImageAsync() newPackageDataCache.ProfileImage = CreateCacheData(newProfileImage, tick()) --Make sure the user doesn't change, then --if the user worn/purchased while we update in BG, we damp the data as it may be stale if startCount == UserChangedCount and not visiblePackageUpdating then --New CachedData InternalPackageDataCache = newPackageDataCache InternalPackageDataCache.Version = tick() --VisiblePackageDataCache is the CacheData used in app and will turn into visible elements --user can operate on these visible elements and update VisiblePackageDataCache like wear/buy and so on --We need the PackageDataCache to init VisiblePackageDataCache if not VisiblePackageDataCache then VisiblePackageDataCache = deepcopy(InternalPackageDataCache) VisiblePackageDataCache.OnProfileImageUpdateBegin = Utility.Signal() VisiblePackageDataCache.OnProfileImageUpdateEnd = Utility.Signal() VisiblePackageDataCache.OnDifferentWearing = Utility.Signal() VisiblePackageDataCache.OnDifferentOwned = Utility.Signal() --Each single pacakge can fire AvatarEquipSuccess and AvatarPurchaseSuccess. --As whenever one single package data changes, other packages(previous wearing/not owned) may also be influenced, --so we need listen to the signals to update all packages wearing and owned EventHub:removeEventListener(EventHub.Notifications["AvatarEquipBegin"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseBegin"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["AvatarEquipSuccess"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "VisiblePackageDataCache") --Listen to Character update/equip from Avatar Editor EventHub:removeEventListener(EventHub.Notifications["CharacterUpdated"], "VisiblePackageDataCache") EventHub:removeEventListener(EventHub.Notifications["CharacterEquipped"], "VisiblePackageDataCache") EventHub:addEventListener(EventHub.Notifications["AvatarEquipBegin"], "VisiblePackageDataCache", function(assetId) visiblePackageUpdating = true end) EventHub:addEventListener(EventHub.Notifications["AvatarPurchaseBegin"], "VisiblePackageDataCache", function(assetId) visiblePackageUpdating = true end) EventHub:addEventListener(EventHub.Notifications["AvatarEquipSuccess"], "VisiblePackageDataCache", function(assetId, skipProfileImageUpdate) local startCount = UserChangedCount UpdateDataWearing(VisiblePackageDataCache, assetId) VisiblePackageDataCache.OnDifferentWearing:fire(assetId) spawn(PreloadCharacterAppearanceAsync) if not skipProfileImageUpdate then --Update Profile Image for wearing ProfileImageUpdateForWearing = ProfileImageUpdateForWearing + 1 local thisProfileImageUpdateForWearing = ProfileImageUpdateForWearing VisiblePackageDataCache.OnProfileImageUpdateBegin:fire() visiblePackageUpdating = true local newProfileImage = PackageData:GetProfileImageAsync() if startCount == UserChangedCount and VisiblePackageDataCache then if thisProfileImageUpdateForWearing == ProfileImageUpdateForWearing and newProfileImage then UpdateDataProfileImage(VisiblePackageDataCache, newProfileImage) VisiblePackageDataCache.OnProfileImageUpdateEnd:fire(newProfileImage) else VisiblePackageDataCache.OnProfileImageUpdateEnd:fire() end end end end) EventHub:addEventListener(EventHub.Notifications["AvatarPurchaseSuccess"], "VisiblePackageDataCache", function(assetId, owned) UpdateDataOwned(VisiblePackageDataCache, assetId, owned) VisiblePackageDataCache.OnDifferentOwned:fire(assetId, owned) end) --CharacterUpdate: If anything changed on character, reload the profile image EventHub:addEventListener(EventHub.Notifications["CharacterUpdated"], "VisiblePackageDataCache", function() local startCount = UserChangedCount --Update Profile Image for wearing ProfileImageUpdateForWearing = ProfileImageUpdateForWearing + 1 local thisProfileImageUpdateForWearing = ProfileImageUpdateForWearing VisiblePackageDataCache.OnProfileImageUpdateBegin:fire() visiblePackageUpdating = true local newProfileImage = PackageData:GetProfileImageAsync() if startCount == UserChangedCount and VisiblePackageDataCache then if thisProfileImageUpdateForWearing == ProfileImageUpdateForWearing and newProfileImage then UpdateDataProfileImage(VisiblePackageDataCache, newProfileImage) VisiblePackageDataCache.OnProfileImageUpdateEnd:fire(newProfileImage) else VisiblePackageDataCache.OnProfileImageUpdateEnd:fire() end end end) --If we equipped any asset on character, make wearing update EventHub:addEventListener(EventHub.Notifications["CharacterEquipped"], "VisiblePackageDataCache", function(newAssets, skipProfileImageUpdate) --Get assets array from store assets local assetsArray = {} for _, assetIds in pairs(newAssets) do for _, assetId in pairs(assetIds) do table.insert(assetsArray, tonumber(assetId)) end end table.sort(assetsArray) local newWearingAssetId = nil local curWearingAssetId = nil local packages = VisiblePackageDataCache.Packages.Data --Compare package assetids with wearing assetids for _, package in pairs(packages) do if package:IsWearing() then curWearingAssetId = tonumber(package:GetAssetId()) end end for _, package in pairs(packages) do if CompareAssetIdArrays(package:GetPartIdsAsync(), assetsArray) then newWearingAssetId = tonumber(package:GetAssetId()) break end end if curWearingAssetId ~= newWearingAssetId then --Fire AvatarEquipSuccess event for achievement and wearing update EventHub:dispatchEvent(EventHub.Notifications["AvatarEquipSuccess"], newWearingAssetId, skipProfileImageUpdate) end end) end validUpdate = true end end return validUpdate end --"Sync" call to get VisiblePackageDataCache unless VisiblePackageDataCache is not initialized function PackageData:GetCachedData() --If not cached data, we wait until UpdateCachedData is done if not VisiblePackageDataCache then while debounceUpdateCachedData do wait() end --If still no data, try to fetch VisiblePackageDataCache again manually if not VisiblePackageDataCache then ReloaderManager:callReloaderFunc("PackageData", UpdateFuncId) end else if InternalPackageDataCache then --We merge VisiblePackageDataCache with InternalPackageDataCache if it's Version is behind --use Update to make sure we are using the latest cached data if VisiblePackageDataCache.Version < InternalPackageDataCache.Version then VisiblePackageDataCache.Packages:Update(InternalPackageDataCache.Packages) VisiblePackageDataCache.WearingAssetId:Update(InternalPackageDataCache.WearingAssetId) VisiblePackageDataCache.ProfileImage:Update(InternalPackageDataCache.ProfileImage) for assetId,_ in pairs(InternalPackageDataCache.OwnedInfo) do if VisiblePackageDataCache.OwnedInfo[assetId] then VisiblePackageDataCache.OwnedInfo[assetId]:Update(InternalPackageDataCache.OwnedInfo[assetId]) else VisiblePackageDataCache.OwnedInfo[assetId] = InternalPackageDataCache.OwnedInfo[assetId] end end VisiblePackageDataCache.Version = InternalPackageDataCache.Version end end end if VisiblePackageDataCache then --Update Packages Owned and Wearing based on the merged data for _, package in pairs(VisiblePackageDataCache.Packages.Data) do local assetId = package:GetAssetId() local wearingAssetId = VisiblePackageDataCache.WearingAssetId.Data if VisiblePackageDataCache.OwnedInfo[assetId].Data then package:SetOwned(true) package:SetWearing(wearingAssetId == assetId) else package:SetOwned(false) package:SetWearing(false) end end end return VisiblePackageDataCache end --Check whether the VisiblePackageDataCache has been initialized function PackageData:HasCachedData() return VisiblePackageDataCache ~= nil end --Update Avatar Pane for purchasing consumables local debounceUpdatePurchasedConsumables = false function PackageData:UpdatePurchasedConsumablesAsync(purchasedConsumablesInfo) local startCount = UserChangedCount --Wait until we get VisiblePackageDataCache if not VisiblePackageDataCache then while debounceUpdateCachedData do wait() end end while debounceUpdatePurchasedConsumables do wait() end debounceUpdatePurchasedConsumables = true if startCount == UserChangedCount and purchasedConsumablesInfo and #purchasedConsumablesInfo > 0 and VisiblePackageDataCache then --Check if any consumable is an Avatar local purchasedAvatarAssetIdMap = {} local purchasedAvatar = false for _, consumable in pairs(purchasedConsumablesInfo) do if consumable and tostring(consumable['Type']) == 'Avatar' then purchasedAvatarAssetIdMap[tostring(consumable['Roblox_AssetId'])] = true purchasedAvatar = true end end if purchasedAvatar then local consumedAssetIds = {} for _, package in pairs(VisiblePackageDataCache.Packages.Data) do local assetId = package:GetAssetId() if purchasedAvatarAssetIdMap[tostring(assetId)] then table.insert(consumedAssetIds, assetId) end end --if user consumed Avatar and still in the same session, we update the owned if startCount == UserChangedCount and VisiblePackageDataCache then --inform the BG update that we are updating the visible packages visiblePackageUpdating = true for i = 1, #consumedAssetIds do EventHub:dispatchEvent(EventHub.Notifications["AvatarPurchaseSuccess"], consumedAssetIds[i], true) end end end end debounceUpdatePurchasedConsumables = false end end return PackageData