This tutorial covers on how to make a Gifting Gamepass System that works even with the player in-game, or offline + uses the new UserPriceLevel API for regionally priced gifts.
You can use this tutorial if:
- You already have a Player Data Saving system and want to integrate Gamepass Gifting into it
- If you want a Player Data Saving system from scratch that is compatible with Gamepass Gifting
NOTE: This is ONLY a tutorial, you may use pieces of the tutorial’s code, however some parts are in blank for you to fill in using your game’s systems or systems you have to create yourself (such as notifying a user and applying rate limits on the gift remote)
There may be mistakes in the code or hard-to-read sections, if you stumble upon one of them, please comment in this post
Gamepasses
For this tutorial, a Gamepasses ModuleScript should be present in ReplicatedStorage:
--game.ReplicatedStorage.Gamepasses
local Gamepasses = {
["Jump Boost"] = {
Gamepass = 1577725781,
Product = 3453130190, --Devproduct used for gifting
RegionalProduct = 3453139395 --Also devproduct used for gifting, but with regional pricing
}
}
return Gamepasses
Make sure to change the IDs to your game’s gamepass and product IDs.
Player Data Loading/Saving
You can either implement this into your existing data handling script or follow this tutorial to create a new one.
Create a ModuleScript named “PlayerData” in ServerScriptService. These are the initial variables that are gonna be necessary:
local PlayerDataModule = {}
local DataStore = game:GetService("DataStoreService"):GetDataStore("PlayerData")
local MessagingService = game:GetService("MessagingService")
local Marketplace = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local Gamepasses = require(game.ReplicatedStorage.Gamepasses)
local DataCache = {}
local DefaultData = {
Coins = 0, --Example, won't be used
Items = {}, --Example, won't be used
Gamepasses = {},
Purchases = {}
}
(If you have your own system already, just add the initial variables to it, replace DataStore with your datastore and DefaultData with your system’s Default Data, but make sure it has “Gamepasses” and “Purchases” in it)
Next, you need the RetryRequest function which can retry unsafe calls (like DataStore and Marketplace calls) and also BetterTableClone, which is table.clone but it also copies the tables inside the root table:
function RetryRequest(Object:Instance, Function:string, ...)
local MaxRetries = 5 --Change this to whatever you prefer
local Cooldown = 1
local Tries = 0
local ErrorMessage = "Unknown Error"
while Tries <= MaxRetries do
Tries += 1
local Result = {pcall(Object[Function], Object, ...)}
if Result[1] then
--Request resulted in success!
return unpack(Result)
else
ErrorMessage = Result[2] or ErrorMessage
end
task.wait(Cooldown)
end
return false, ErrorMessage
end
--We need this for cloning tables that have tables inside them
--(Advanced developers will understand why)
function BetterTableClone(Table)
local copy = {}
for key, value in Table do
if type(value) == "table" then
copy[key] = BetterTableClone(value) -- recursion
else
copy[key] = value
end
end
return copy
end
Now you need a function that handles players when they join the game, if you already have a function that does that great, but if you are following this tutorial step by step then here’s one you can use and it’s steps:
- Check’s if the player’s data is already loaded in this server, and doesn’t load it if it already is.
- Tries to get the player’s data from datastore, if the player never joined the game Default Data is used instead.
- Checks if the player left while step 2 (getting data from datastore) was happening, and halts if the player did leave.
- Goes (iterates) through every gamepass in the Gamepasses module and checks if the player has them, if yes, then it gets added to the player’s passes
Code for the PlayerJoined function:
--When a player joins, we call this function
function PlayerJoined(Player:Player)
local UserId = Player.UserId::number
if DataCache[UserId] then
return false --Unlikely, but the player's data could still be loaded when they rejoin
end
--We retrieve the data from roblox's saving system
local Success, Data = RetryRequest(DataStore, "GetAsync", UserId)
if not Success then
--Roblox services can be down at any time
--Make sure to warn the player of this error!
----In this case, Data is the Error Message
return warn("Error loading data for player "..Player.Name..": "..Data)
end
print(Player.Name.."'s data loaded")
if not Data then
--First time this player is joining! Let's give them the default data
Data = BetterTableClone(DefaultData)
end
if not Player.Parent then
--Player left while we were retrieving their data, let's end here
return false
end
DataCache[UserId] = Data
--Now we iterate through the gamepasses and check if they have them
local PlayerGamepasses = Data.Gamepasses
for Name, GamepassData in Gamepasses do --Don't get PlayerGamepasses & Gamepasses mixed up! They are different!
if not PlayerGamepasses[Name] then --Player doesn't have the gamepass!
local GamepassId = GamepassData.Gamepass
local Success, HasPass = pcall(Marketplace.UserOwnsGamePassAsync, Marketplace, UserId, GamepassId)
if Success and HasPass then
PlayerGamepasses[Name] = true
end
end
end
return Data
end
If you already have your own saving system, make sure to add this section of code after the player’s data is already loaded:
----Data = Player's Data that just got loaded
--Now we iterate through the gamepasses and check if the player has them
local PlayerGamepasses = Data.Gamepasses
for Name, GamepassData in Gamepasses do --Don't get PlayerGamepasses & Gamepasses mixed up! They are different!
if not PlayerGamepasses[Name] then --Player doesn't have the gamepass!
local GamepassId = GamepassData.Gamepass
local Success, HasPass = pcall(Marketplace.UserOwnsGamePassAsync, Marketplace, UserId, GamepassId)
if Success and HasPass then
PlayerGamepasses[Name] = true
end
end
end
To end it off, if your PlayerJoined function isn’t connected to the PlayerAdded signal yet, add this:
--Create the connection so when players join the function gets called
Players.PlayerAdded:Connect(PlayerJoined)
for _,Player in Players:GetPlayers() do
--We call PlayerJoined in a different coroutine so all calls can run simultaneously
task.spawn(PlayerJoined, Player)
end
Now we move on to the saving part, when the player leaves. If you already have a saving system, nothing needs to be changed. If you don’t, here’s a function that does it (The function is really simple so explaining details is not necessary):
function PlayerLeft(Player:Player)
local UserId = Player.UserId::number
if not DataCache[UserId] then
--Player left before their data was loaded or wasn't loaded at all
--In this case, we return instead of overwriting their data
return false
end
local Data = DataCache[UserId]
DataCache[UserId] = nil
local Success, ErrorMessage = RetryRequest(DataStore, "SetAsync", UserId, Data)
if not Success then
--This is a tutorial and does not contain auto-save, you should make an auto-save for your game!
warn("Error saving "..Player.Name.."'s Data: "..ErrorMessage)
else
print(Player.Name.."'s data saved")
end
return Success
end
```
And don't forget to connect it to the PlayerRemoving signal:
```
Players.PlayerRemoving:Connect(PlayerLeft)
```
Gamepass and Gifting system
The following pieces of code are in the same “PlayerData” ModuleScript
Now, whenever the player buys a gamepass in the Gamepasses module, they should get the gamepass in their data. We can do this using Marketplace.PromptGamePassPurchaseFinished:
--When someone finishes buying a gamepass
Marketplace.PromptGamePassPurchaseFinished:Connect(function(Player,GamepassId,WasPurchased)
if not WasPurchased then return end --Ignore if not purchased
local GamepassName
--First we need to find the name of the gamepass using the GamepassId
for Name, GamepassData in Gamepasses do
if GamepassData.Gamepass == GamepassId then
GamepassName = Name
break
end
end
if not GamepassName then
return --Gamepass not found
end
print(Player.Name.."'s bought gamepass: "..GamepassName)
local Data = DataCache[Player.UserId]
if Data then
--We give the player the gamepass!
Data.Gamepasses[GamepassName] = true
end
end)
Now we head to the gifting logic, first we are gonna need a Gift Remote from where the client is gonna send gift requests. For this tutorial, create a RemoteEvent in ReplicatedStorage and name it “GiftGamepass”, if you put it in a different place or named it something else, make sure to define that in the code below, we will also use a GiftIntents table to know who the senders are currently sending gifts to:
--Gifting Logic:
local GiftRemote = game.ReplicatedStorage.GiftGamepass::RemoteEvent
local GiftIntents = {}
Next is the function necessary to check if a gift-sender can send regional gifts to a player, if not then the sender is forced to send the full-priced gift, we use Marketplace:GetUsersPriceLevelsAsync(ArrayOfUserIds) for that, which returns something like:
{
{
UserId = number,
PriceLevel = [1-1000]
},
{
UserId = number,
PriceLevel = [1-1000]
},
}
With that, we can check if the sender’s currency value (PriceLevel) is equal or higher than the receiver’s to determine if the gift will use regional pricing or not (attach this to your script):
function CanSendRegionalGift(Sender,Receiver)
local Success, Data = RetryRequest(Marketplace, "GetUsersPriceLevelsAsync", {Sender, Receiver})
if not Success or not Data then
return false
end
local SenderLevel, ReceiverLevel = 1,1
for _, Info in Data do
if Info.UserId == Sender then
SenderLevel = Info.PriceLevel
else
ReceiverLevel = Info.PriceLevel
end
end
return SenderLevel >= ReceiverLevel
end
Now starts the true gifting system, now we need to receive gift requests from the client using our GiftGamepass remote, and also determine if they are valid (avoid exploiters), here are the steps we need to do:
(Exploit warning: you have to implement rate limit yourself for this request or exploiters can spam your datastore requests)
- Receive the client’s request with parameters GiftingTo (UserId, number) and GamepassName (string)
- Filter the client’s parameters to make sure the Gamepass with given GamepassName exists and that GiftingTo isn’t a fake UserId
- Try to get the data from the player who is getting gifted, if that player doesn’t have data, we assume that player never bought any gamepass and continue, but if that player does then:
-
- If Data.Gamepasses contains GamepassName as true, then return and halt the request
-
- If Data.Gamepasses doesn’t contain GamepassName, then the player never bought the gamepass, and continue with the request
- Set GiftIntents[Player] to GiftingTo (memory leak warning: this is only a tutorial, the following piece of code doesn’t remove GiftIntents[Player] when the player leaves for simplicity reasons)
- Check if the sender can gift regionally, if yes, prompt them the regional product, if not, then prompt the normal product (full priced)
This is the piece of code following the steps above:
GiftRemote.OnServerEvent:Connect(function(Player:Player, GiftingTo:number, GamepassName:string)
--WARNING: This part of the tutorial has no rate-limit, and can be
--abused by exploiters to fill DataStore rate-limits.
--GiftingTo is an UserId!
GiftingTo = tonumber(GiftingTo)
if not GiftingTo or not GamepassName then
return --Exploiter might be trying to break the server code
end
local GamepassData = Gamepasses[GamepassName]
--If you already have a saving system, make sure to replace the line below with your system's equivalent
local Success, Data = RetryRequest(DataStore, "GetAsync", GiftingTo)
if Success and Data and Data.Gamepasses[GamepassName] then
return false --Gifting-to player already owns the gamepass!
--Make sure to add a way so Player can know that who they are gifting-to already has the gamepass
elseif not Success then
return false --To make sure, we will return, what if the gifting-to player has the gamepass?
end
--MEMORY LEAK WARNING: This part of the tutorial has no prevetion agaisn't memory leaks
--GiftIntents might get overfilled overtime even though the players in it already left
GiftIntents[Player] = GiftingTo
local Regional = GamepassData.RegionalProduct and CanSendRegionalGift(Player.UserId, GiftingTo)
if Regional then
Marketplace:PromptProductPurchase(Player, GamepassData.RegionalProduct)
else
Marketplace:PromptProductPurchase(Player, GamepassData.Product)
end
end)
Next, we need to use Messaging Service to SubscribeAsync to the topic “GamepassGift”, this is how we are gonna receive gift-requests if the player is in-game. (If you already have a saving system, make sure to replace the datastore-saving part with your system’s equivalent):
task.spawn(function() --Spawn in a different coroutine so that it doesn't delay other scripts when requiring this module
--When someone receives a gamepass gift while they are in-game
RetryRequest(MessagingService, "SubscribeAsync", "GamepassGift", function(Packet)
local Sent, Data = Packet.Sent, Packet.Data
--Sent is UNIX Timestamp of when the gamepass was gifted
--Data is the data of the gift received
local GiftedTo = Data.GiftingTo
local Player = game.Players:GetPlayerByUserId(Data.GiftingTo)
if Player then
--Now we need to get their player data
local PlayerData = DataCache[GiftedTo]
if not PlayerData then
repeat
task.wait()
PlayerData = DataCache[GiftedTo]
until PlayerData or not Player.Parent
if not Player.Parent then
--Player left
return
end
end
print(Player.Name.." received gift: "..Data.GamepassName)
PlayerData.Gamepasses[Data.GamepassName] = true
--If you already have a saving system, modify the line below to match with your saving system
RetryRequest(DataStore, "SetAsync", GiftedTo, PlayerData)
local GiftedBy = Data.UserId
--Notify the Gifted-To player about their gift
end
end)
end)
Now, we need to handle when players press buy after getting prompted with the gift from the GiftRemote’s code. For this, we use Marketplace.ProcessReceipt. Warning: ProcessReceipt can only be used once per game, so if in your game ProcessReceipt is already used, move it to the PlayerData module or integrate the PlayerData’s logic into your ProcessReceipt, explaining what the following code does would take too many steps, but the code has lots of comments so you can try to read off that:
--When someone buys a dev-product (gift)
Marketplace.ProcessReceipt = function(Info)
local PurchaseId = Info.PurchaseId or tostring(math.random(10000,100000000))
local UserId = Info.PlayerId
local ProductId = Info.ProductId
local RobuxSpent = Info.CurrencySpent or 0
local Player = Players:GetPlayerByUserId(UserId)
if not Player then
--Don't worry, when player rejoins the server will retry to process the gift
return Enum.ProductPurchaseDecision.NotProcessedYet
end
local IsRegionalGift = false
--We need to get the name of the gamepass now
local GamepassName
for Name, GamepassData in Gamepasses do
if GamepassData.Product == ProductId then
GamepassName = Name
break
elseif GamepassData.RegionalProduct == ProductId then
IsRegionalGift = true
GamepassName = Name
break
end
end
if not GamepassName then
return Enum.ProductPurchaseDecision.NotProcessedYet --Weird behavior
end
print(Player.Name.."'s bought gift: "..GamepassName)
--We need to get the player's data so we can put this purchase in his purchases
local PlayerData = DataCache[UserId]
if not PlayerData then
repeat
task.wait()
PlayerData = DataCache[UserId]
until PlayerData or not Player.Parent
if not Player.Parent then
--Player left
return Enum.ProductPurchaseDecision.NotProcessedYet
end
end
--Track purchase
local Purchase = PlayerData.Purchases[PurchaseId]
if not Purchase then
Purchase = {
Time = os.time(),
PurchaseId = PurchaseId,
ProductId = ProductId,
GamepassName = GamepassName,
RobuxSpent = RobuxSpent,
IsRegional = IsRegionalGift
}
PlayerData.Purchases[PurchaseId] = Purchase
end
--Get who the player is gifting to
local GiftingTo = Purchase and Purchase.GiftingTo or GiftIntents[Player]
if not GiftingTo then
--Player might've rejoined but didn't choose someone to gift yet
-- <-- Add a way here to warn the player to choose someone to gift
repeat
task.wait()
GiftingTo = GiftIntents[Player]
until GiftingTo or not Player.Parent
if not Player.Parent then
--Player left before choosing someone to gift
return Enum.ProductPurchaseDecision.NotProcessedYet
end
end
PlayerData.Purchases[PurchaseId].GiftingTo = GiftingTo
if IsRegionalGift then
local CanSendRegional = CanSendRegionalGift(UserId, GiftingTo)
if not CanSendRegional then
--Player tried to gift someone he can't gift
--Either an exploiter or really unlucky
Purchase.Denied = true
return Enum.ProductPurchaseDecision.PurchaseGranted
end
end
--First let's update the gifting-to player's data
local Success1 = RetryRequest(DataStore, "UpdateAsync", GiftingTo, function(Data)
--Player might have never joined the game before,
--so we throw in a 'DefaultData' just to make sure
Data = Data or BetterTableClone(DefaultData)
Data.Gamepasses[GamepassName] = true
--Gamepass given!!!
return Data
end)
--Now send a message to all live servers in-case the player is in-game
local Success2 = RetryRequest(MessagingService, "PublishAsync", "GamepassGift", {
UserId = UserId,
GiftingTo = GiftingTo,
GamepassName = GamepassName,
ProductId = ProductId
})
if not Success1 or not Success2 then
--When the player rejoins, it will retry again
--Notify the player that he should rejoin to retry gifting!
return Enum.ProductPurchaseDecision.NotProcessedYet
end
return Enum.ProductPurchaseDecision.PurchaseGranted
end
And to finally end with the Server Side Gifting Logic, set these last 2 functions in your saving system/playerdata module:
PlayerDataModule.GetUserCache = function(UserId)
return DataCache[UserId]
end
PlayerDataModule.UserHasGamepass = function(UserId,GamepassName,DoWait)
local Player = game.Players:GetPlayerByUserId(UserId) or {}
local PlayerData = DataCache[UserId]
if not PlayerData then
if DoWait then
repeat
task.wait()
PlayerData = DataCache[UserId]
until PlayerData or not Player.Parent
if not Player.Parent then
--Player left the game
return false
end
else
--PlayerData hasn't loaded in yet
return false
end
end
return PlayerData.Gamepasses[GamepassName] or false
end
--If you integrated this tutorial into your existing system, change PlayerDataModule to the name of your module or set it to a BindableFunction
Client Side Example
(LocalScript placed in StarterPlayer/StarterPlayerScripts)
This is not permanent code and you shouldn’t use this unless for testing:
- Adds !gift <username> command in chat
- Adds !buy command in chat
local GAMEPASS_NAME = "Jump Boost"
local Marketplace = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local Player = Players.LocalPlayer
local Gamepasses = require(game.ReplicatedStorage:WaitForChild("Gamepasses"))
local GiftRemote = game.ReplicatedStorage:WaitForChild("GiftGamepass")
Player.Chatted:Connect(function(Message)
if Message:sub(1,5) == "!gift" then
local Username = Message:split(" ")[2] or ""
local Success, UserId = pcall(Players.GetUserIdFromNameAsync, Players, Username)
if Success and UserId then
GiftRemote:FireServer(UserId, GAMEPASS_NAME)
else
warn(UserId)
end
elseif Message:sub(1,4) == "!buy" then
Marketplace:PromptGamePassPurchase(Player, Gamepasses[GAMEPASS_NAME].Gamepass)
end
end)
Implementing the new gamepass checks
Finally, to test out the system, you can try using it from another script: PlayerDataModule.UserHasGamepass(UserId, PassName, DoWait).
Example of a jump boost script, which is placed separately from the PlayerData module:
--Require our Player Data module
local PlayerDataModule = require(game.ServerScriptService.PlayerData)
local function PlayerAdded(Player:Player)
--If the player's character already exists, reload
if Player.Character then
task.spawn(Player.LoadCharacter,Player)
end
--When player's character spawns, we check if they have the gamepass and give jump boost
Player.CharacterAdded:Connect(function(Character)
if PlayerDataModule.UserHasGamepass(Player.UserId, "Jump Boost", true) then
Character:WaitForChild("Humanoid").JumpPower *= 3
end
end)
end
local Players = game:GetService("Players")
--Whenever a player joins we call PlayerAdded
Players.PlayerAdded:Connect(PlayerAdded)
for _, Player in Players:GetPlayers() do
task.spawn(PlayerAdded, Player)
end
Test Place for the system: GiftingSystemTutorial.rbxl (80.6 KB)
Change the GamepassIds and ProductIds in ReplicatedStorage/Gamepasses
Test Commands:
- Use !gift <username> to gift
- Use !buy to prompt the normal non-gift gamepass for you