### Handle Developer Product Purchases with ProfileStore Source: https://madstudioroblox.github.io/ProfileStore/devproducts This example shows how to process developer product purchases using Roblox's MarketplaceService and ProfileStore. It defines functions to handle specific product IDs, awarding in-game items or benefits. Ensure `local Profiles` is initialized as shown in the Basic Usage example. ```lua local Profiles: {[player]: typeof(PlayerStore:StartSessionAsync())} = {} -- See Tutorial > Basic Usage local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local productFunctions = {} -- Example: product ID 456456 awards 100 cash to the user productFunctions[456456] = function(receipt, player, profile) profile.Data.Cash += 100 -- We made changes to the player profile - perform an instant -- save to secure a player purchase against server crashes: profile:Save() end -- Example: product ID 123123 brings the user back to full health productFunctions[123123] = function(receipt, player, profile) local character = player.Character local humanoid = character and character:FindFirstChildWhichIsA("Humanoid") if humanoid then humanoid.Health = humanoid.MaxHealth -- Indicates a successful purchase return true end end local function processReceipt(receiptInfo) local userId = receiptInfo.PlayerId local productId = receiptInfo.ProductId local player = Players:GetPlayerByUserId(userId) if player then local profile = Profiles[player] while profile == nil and player.Parent == Players do profile = Profiles[player] if profile ~= nil then break end task.wait() end if profile ~= nil and profile:IsActive() == true then -- Gets the handler function associated with the developer product ID and attempts to run it local handler = productFunctions[productId] local success, result = pcall(handler, receiptInfo, player, profile) if success then -- The user has received their items -- Returns "PurchaseGranted" to confirm the transaction return Enum.ProductPurchaseDecision.PurchaseGranted else warn(`Failed to process receipt:`, receiptInfo, result) end end end -- The user's items couldn't be awarded -- Returns "NotProcessedYet" and tries again next time the user joins the experience return Enum.ProductPurchaseDecision.NotProcessedYet end -- Sets the callback -- This can only be done once by one server-side script MarketplaceService.ProcessReceipt = processReceipt ``` -------------------------------- ### StartSessionAsync Example Usage Source: https://madstudioroblox.github.io/ProfileStore/api Demonstrates using the Cancel parameter to stop session requests if a player leaves during the loading process. ```lua local Players = game:GetService("Players") local profile = PlayerStore:StartSessionAsync(tostring(player.UserId), { Cancel = function() return player:IsDescendantOf(Players) == false end, }) ``` -------------------------------- ### Initialize and Manage Player Profiles with ProfileStore Source: https://madstudioroblox.github.io/ProfileStore/tutorial This is a standard implementation for initializing ProfileStore, setting up a profile template, and managing player sessions. It handles player joining and leaving, auto-saving data, and provides an example of granting in-game currency. ```lua local ProfileStore = require(game.ServerScriptService.ProfileStore) -- The PROFILE_TEMPLATE table is what new profile "Profile.Data" will default to: local PROFILE_TEMPLATE = { Cash = 0, Items = {}, } local Players = game:GetService("Players") local PlayerStore = ProfileStore.New("PlayerStore", PROFILE_TEMPLATE) local Profiles: {[Player]: typeof(PlayerStore:StartSessionAsync())} = {} local function PlayerAdded(player) -- Start a profile session for this player's data: local profile = PlayerStore:StartSessionAsync(`{player.UserId}`, { Cancel = function() return player.Parent ~= Players end, }) -- Handling new profile session or failure to start it: if profile ~= nil then profile:AddUserId(player.UserId) -- GDPR compliance profile:Reconcile() -- Fill in missing variables from PROFILE_TEMPLATE (optional) profile.OnSessionEnd:Connect(function() Profiles[player] = nil player:Kick(`Profile session end - Please rejoin`) end) if player.Parent == Players then Profiles[player] = profile print(`Profile loaded for {player.DisplayName}!`) -- EXAMPLE: Grant the player 100 coins for joining: profile.Data.Cash += 100 -- You should set "Cash" in PROFILE_TEMPLATE and use "Profile:Reconcile()", -- otherwise you'll have to check whether "Data.Cash" is not nil else -- The player has left before the profile session started profile:EndSession() end else -- This condition should only happen when the Roblox server is shutting down player:Kick(`Profile load fail - Please rejoin`) end end -- In case Players have joined the server earlier than this script ran: for _, player in Players:GetPlayers() do task.spawn(PlayerAdded, player) end Players.PlayerAdded:Connect(PlayerAdded) Players.PlayerRemoving:Connect(function(player) local profile = Profiles[player] if profile ~= nil then profile:EndSession() end end) ``` -------------------------------- ### ProfileStore:StartSessionAsync() Source: https://madstudioroblox.github.io/ProfileStore/api Starts a session for a profile, allowing data to be saved automatically. Handles session locking to prevent data conflicts. ```APIDOC ## ProfileStore:StartSessionAsync() ### Description Starts a session for a profile. If other servers call this method using the same `profile_key`, they will be notified to make a final save before another server acquires the session. While a session is active, changes to `Profile.Data` are saved automatically. You must call `Profile:EndSession()` when done to prevent excessive DataStore requests. ### Method `ProfileStore:StartSessionAsync(profile_key, params?)` ### Parameters #### Path Parameters - **profile_key** (string) - The DataStore key for the profile. - **params** (table, optional) - Additional rules for the session start request. - **Cancel** (function, optional) - A function called periodically to check if the session is still needed. Returns `true` if the session should be cancelled. Disables the default timeout. - **Steal** (boolean, optional) - If `true`, bypasses session locks and immediately starts a session. **Use with caution**, as it bypasses anti-duplication measures. Primarily for debugging. ### Request Example ```lua local Players = game:GetService("Players") local profile = PlayerStore:StartSessionAsync(tostring(player.UserId), { Cancel = function() return player:IsDescendantOf(Players) == false end, }) ``` ### Response - **Profile** (`Profile` object) or **nil** - Returns a `Profile` object if the session starts successfully, otherwise `nil`. ### Notes - `ProfileStore` saves profiles to live DataStore keys in Roblox Studio when Roblox API services are enabled. Use `ProfileStore.Mock` for testing without live saves. - `:StartSessionAsync()` can return `nil` if another server starts a session for the same profile concurrently. In this rare case, consider kicking the player. ``` -------------------------------- ### GetAsync Method Source: https://madstudioroblox.github.io/ProfileStore/api Retrieves profile data without starting a session. Data returned is read-only regarding auto-saving. ```lua ProfileStore:GetAsync(profile_key, version?) --> [Profile] or nil -- profile_key [string] -- version nil or [string] -- DataStore key version ``` -------------------------------- ### Rollback profile data to a previous version Source: https://madstudioroblox.github.io/ProfileStore/api Example of querying a profile version before a specific date and performing a rollback using SetAsync. ```lua -- Get a ProfileStore object with the same arguments you passed to the -- ProfileStore that loads player Profiles: local PlayerStore = ProfileStore.New("PlayerData", {}) -- If you can't figure out the exact time and timezone the player lost coins -- in on the day of August 14th, then your best bet is to try querying -- UTC August 13th. If the first entry still doesn't have the coins - -- try a new query of UTC August 12th and etc. local max_date = DateTime.fromUniversalTime(2021, 08, 13) -- UTC August 13th, 2021 local query = PlayerStore:VersionQuery( "Player_2312310", -- The same profile key that gets passed to :LoadProfileAsync() Enum.SortDirection.Descending, nil, max_date ) -- Get the first result in the query: local profile = query:NextAsync() if profile ~= nil then profile:SetAsync() -- This method does the actual rolling back; -- Don't call this method until you're sure about setting the latest -- version to a copy of the previous one print(`Rollback success!`) print(profile.Data) -- You'll be able to surf table contents if -- you're running this code in studio with access to API services -- enabled and have expressive output enabled; If the printed -- data doesn't have the coins, you'll want to change your -- query parameters. else print(`No version to rollback to`) end ``` -------------------------------- ### Kick Player on Session End Source: https://madstudioroblox.github.io/ProfileStore/api Example of kicking a player when their profile session ends, typically used when their data is loaded on another server. ```lua Profile.OnSessionEnd:Connect(function() player:Kick(`Your data has been loaded on another server - please rejoin`) end) ``` -------------------------------- ### ProfileStore:GetAsync() Source: https://madstudioroblox.github.io/ProfileStore/api Retrieves a profile's data from the DataStore without starting a session. Useful for reading data without auto-saving. ```APIDOC ## ProfileStore:GetAsync() ### Description Attempts to load the latest profile version (or a specified version) from the DataStore without starting a session. The returned `Profile` object will not auto-save, and `:EndSession()` does not need to be called for it. Data can be edited and saved later using `Profile:SetAsync()`. ### Method `ProfileStore:GetAsync(profile_key, version?)` ### Parameters #### Path Parameters - **profile_key** (string) - The DataStore key for the profile. - **version** (string, optional) - The specific version of the DataStore key to retrieve. ### Response - **Profile** (`Profile` object) or **nil** - Returns a `Profile` object if data exists for the `profile_key`, otherwise `nil`. `Profile.Data` will not auto-save when using this method. ``` -------------------------------- ### Get First Session Time Source: https://madstudioroblox.github.io/ProfileStore/api Profile.FirstSessionTime is a read-only Unix timestamp indicating when the profile was created. ```lua Profile.FirstSessionTime [number] (read-only) -- Unix time ``` -------------------------------- ### ProfileStore Session Management Source: https://madstudioroblox.github.io/ProfileStore/datause Methods for managing profile sessions, including starting, ending, and auto-saving data. ```APIDOC ## :StartSessionAsync() ### Description Starts a session for a profile. Handles session conflicts using MessagingService and DataStore updates. ### Method Internal ### Parameters - None ### Response - **Success**: Session started, profile data loaded. --- ## :EndSession() ### Description Ends an active profile session, triggering a final save. ### Method Internal ### Parameters - None ### Response - **Success**: Session terminated and data persisted. ``` -------------------------------- ### Get Session Load Count Source: https://madstudioroblox.github.io/ProfileStore/api Profile.SessionLoadCount is a read-only number indicating how many times a session has been started for this profile. ```lua Profile.SessionLoadCount [number] (read-only) ``` -------------------------------- ### Handle Logout Penalties with Profile.OnLastSave Source: https://madstudioroblox.github.io/ProfileStore/api Example of penalizing a player if they log out while in combat, using the OnLastSave signal. Ensure critical logic is not solely dependent on this signal due to potential server crashes. ```lua local InCombat = false Profile.OnLastSave:Connect(function(reason) if reason ~= "Shutdown" then print(`The cause of the session ending is not due to a server shutdown`) -- If you didn't want the player to logout at this particular moment, -- this should be where you'd penalize the player. e.g.: if InCombat == true then Profile.Data.Coins -= 100 end end end) ``` -------------------------------- ### Convert UserID to String Key Source: https://madstudioroblox.github.io/ProfileStore/troubleshooting When storing non-sequential numbers as table indexes, convert them to string keys to ensure compatibility with DataStore serialization. This example shows converting a user ID. ```lua Profile.Data.Friends[tostring(user_id)] = {GoodFriend = true} ``` -------------------------------- ### Access Session Information Source: https://madstudioroblox.github.io/ProfileStore/api Profile.Session is a read-only table containing PlaceId and JobId of the server where the session started. It may be nil if no session is active or if the profile was just read. This value never changes after profile creation. ```lua Profile.Session [table?] (read-only) -- nil or {PlaceId = number, JobId = string} ``` -------------------------------- ### Check UserData Type Source: https://madstudioroblox.github.io/ProfileStore/troubleshooting Use this check to determine if a variable holds UserData, which cannot be directly serialized by DataStore. Examples include Instances, Vector3, CFrame, and Udim2. ```lua print(type(value) == "userdata") ``` -------------------------------- ### Initialize a New ProfileStore Source: https://madstudioroblox.github.io/ProfileStore/api Create a new ProfileStore instance. The 'template' argument is optional and provides a default structure for 'Profile.Data' if no data has been saved previously. ```lua ProfileStore.New(store_name, template?) -- store_name [string] -- DataStore name -- template nil or [table] -- Profile.Data will default -- to given table (deep-copy) when no data was saved previously ``` -------------------------------- ### ProfileStore Initialization and Configuration Source: https://madstudioroblox.github.io/ProfileStore/api Methods for creating new ProfileStore instances and configuring internal constants. ```APIDOC ## ProfileStore Initialization and Configuration ### .New() ```lua ProfileStore.New(store_name, template?) -- store_name [string] -- DataStore name -- template nil or [table] -- Profile.Data will default -- to given table (deep-copy) when no data was saved previously ``` `ProfileStore` objects expose methods for reading and writing to profiles. Equivalent of :GetDataStore() in Roblox DataStoreService API. Notice By default, `template` is only copied for `Profile.Data` for new profiles. Changes made to `template` can be applied to `Profile.Data` of previously saved profiles by calling Profile:Reconcile(). Using templates and reconciliation is completely optional and you may alter `Profile.Data` with your own code alone. ### .SetConstant() ```lua ProfileStore.SetConstant(name, value) -- name [string] "AUTO_SAVE_PERIOD" | "LOAD_REPEAT_PERIOD" | "FIRST_LOAD_REPEAT" | "SESSION_STEAL" | -- "ASSUME_DEAD" | "START_SESSION_TIMEOUT" | "CRITICAL_STATE_ERROR_COUNT" | "CRITICAL_STATE_ERROR_EXPIRE" | -- "CRITICAL_STATE_EXPIRE" | "MAX_MESSAGE_QUEUE" -- value [number] ``` A feature for experienced developers who understand how ProfileStore works for changing internal constants without having to fork the ProfileStore project. ``` -------------------------------- ### StartSessionAsync Method Source: https://madstudioroblox.github.io/ProfileStore/api Initiates a session for a profile key. Always call Profile:EndSession() after completion to prevent excessive DataStore requests. ```lua ProfileStore:StartSessionAsync(profile_key, params?) --> [Profile] or nil -- profile_key [string] -- DataStore key -- params nil or [table]: { -- Cancel: fn() -> (boolean)? -- Steal: boolean? -- } ``` -------------------------------- ### Access DataStoreKeyInfo Source: https://madstudioroblox.github.io/ProfileStore/api Profile.KeyInfo provides the DataStoreKeyInfo instance related to this profile. ```lua Profile.KeyInfo [DataStoreKeyInfo] ``` -------------------------------- ### ProfileStore:VersionQuery() Source: https://madstudioroblox.github.io/ProfileStore/api Creates a profile version query. Results are retrieved through VersionQuery:NextAsync(). Date definitions are easier with the DateTime library. User-defined day and time will have to be converted to Unix time while taking their timezone into account for precise results. ```APIDOC ## ProfileStore:VersionQuery() ### Description Creates a profile version query using DataStore:ListVersionsAsync(). Results are retrieved through `VersionQuery:NextAsync()`. ### Method `ProfileStore:VersionQuery(profile_key, sort_direction?, min_date?, max_date?)` ### Parameters #### Path Parameters - **profile_key** (string) - Required - The unique key identifying the profile. - **sort_direction** (Enum.SortDirection | nil) - Optional - The direction to sort the versions. Defaults to `Enum.SortDirection.Ascending`. - **min_date** (DateTime | number | nil) - Optional - The minimum date (inclusive) for the query. Can be a `DateTime` object or epoch time in milliseconds. Defaults to nil. - **max_date** (DateTime | number | nil) - Optional - The maximum date (inclusive) for the query. Can be a `DateTime` object or epoch time in milliseconds. Defaults to nil. ### Request Example ```lua -- Example 1: Find the oldest available version local queryOldest = PlayerStore:VersionQuery("Player_123") local oldestProfile = queryOldest:NextAsync() -- Example 2: Find the most recent version local queryMostRecent = PlayerStore:VersionQuery("Player_123", Enum.SortDirection.Descending) local mostRecentProfile = queryMostRecent:NextAsync() -- Example 3: Find the most recent version before a specific date local specificDate = DateTime.fromUniversalTime(2021, 8, 13) local queryBeforeDate = PlayerStore:VersionQuery("Player_123", Enum.SortDirection.Descending, nil, specificDate) local profileBeforeDate = queryBeforeDate:NextAsync() ``` ### Response Returns a `VersionQuery` object that can be used to fetch profile versions. ### Response Example ```lua -- The VersionQuery object itself is returned, not a direct profile. -- Use query:NextAsync() to retrieve profile versions. local query = PlayerStore:VersionQuery("Player_123") print(typeof(query)) -- Expected output: "table" (representing the VersionQuery object) ``` ### Error Handling - If `profile_key` is invalid or not found, `query:NextAsync()` may return nil. - Invalid date formats or types for `min_date` or `max_date` might lead to errors or unexpected results. ``` -------------------------------- ### Implement MarketplaceService ProcessReceipt Source: https://madstudioroblox.github.io/ProfileStore/devproducts Handles product purchase receipts by validating the PurchaseId and executing associated product functions. Requires a defined ProductFunctions table and a PurchaseIdCheckAsync helper. ```lua warn(`No product function defined for ProductId {receipt_info.ProductId}; Player: {player.Name}`) return Enum.ProductPurchaseDecision.NotProcessedYet end return PurchaseIdCheckAsync( profile, receipt_info.PurchaseId, function() ProductFunctions[receipt_info.ProductId](receipt_info, player, profile) end ) end end return Enum.ProductPurchaseDecision.NotProcessedYet end MarketplaceService.ProcessReceipt = ProcessReceipt ``` -------------------------------- ### Profile:Save() Source: https://madstudioroblox.github.io/ProfileStore/api Immediately saves the profile data to the DataStore. ```APIDOC ## Profile:Save() ### Description Calling `Profile:Save()` will immediately save `Profile.Data` to the DataStore when a profile session is still active (`Profile:IsActive()` returns `true`). This should only be used for critical moments. ### Method `Profile:Save()` ### Endpoint N/A ### Parameters This method does not take any parameters. ### Request Example ```lua Profile:Save() ``` ### Response This method does not return a value. ``` -------------------------------- ### Define VersionQuery parameters Source: https://madstudioroblox.github.io/ProfileStore/api The VersionQuery method signature and parameter definitions for querying profile versions. ```lua ProfileStore:VersionQuery(profile_key, sort_direction?, min_date?, max_date?) --> [VersionQuery] -- profile_key [string] -- sort_direction nil or [Enum.SortDirection] -- Defaults to "Ascending" -- min_date nil or [DateTime] or [number] (epoch time millis) -- max_date nil or [DateTime] or [number] (epoch time millis) ``` -------------------------------- ### Save Profile Data Immediately Source: https://madstudioroblox.github.io/ProfileStore/api Immediately saves the Profile.Data to the DataStore during an active session. Use for critical data like purchases before potential server crashes. This counts as one :UpdateAsync() call. ```lua Profile:Save() ``` -------------------------------- ### Connect to Profile.OnAfterSave Signal Source: https://madstudioroblox.github.io/ProfileStore/api Fires after profile data is successfully saved to the DataStore via GlobalDataStore:UpdateAsync(). Use to verify saved data. ```lua Profile.OnAfterSave:Connect(function(last_saved_data) print(`Profile.Data has been successfully saved to the DataStore:`, last_saved_data) end) ``` -------------------------------- ### Connect to OnOverwrite Signal Source: https://madstudioroblox.github.io/ProfileStore/api Connect to the OnOverwrite signal to detect when a DataStore key returns invalid data types. This can help identify accidental data corruption. ```lua ProfileStore.OnOverwrite:Connect(function(store_name, profile_key) print(`Overwrite has occurred for Store:{store_name}, Key:{profile_key}`) end) ``` -------------------------------- ### ProfileStore Mock Usage for Testing Source: https://madstudioroblox.github.io/ProfileStore/api Utilize ProfileStore.Mock for testing purposes. Mock profiles operate on a separate, temporary data store that is cleared when the game server shuts down, preventing interference with live data. ```lua local PlayerStore = ProfileStore.New("PlayerData", {}) -- This profile would be saved to the DataStore: local LiveProfile = PlayerStore:StartSessionAsync("profile_key") LiveProfile.Data.Value = 1 LiveProfile:EndSession() -- This profile does not load data from the DataStore -- nor save data to the DataStore: -- (This data will disappear after the game server shuts down) local MockProfile = PlayerStore.Mock:StartSessionAsync("profile_key") MockProfile.Data.Value = 1 MockProfile:EndSession() ``` ```lua local RunService = game:GetService("RunService") local PlayerStore = ProfileStore.New("PlayerData", {}) if RunService:IsStudio() == true then PlayerStore = PlayerStore.Mock end ``` -------------------------------- ### Connect to Profile.OnSessionEnd Signal Source: https://madstudioroblox.github.io/ProfileStore/api Fires after a profile session ends. Use this to perform actions like kicking a player if their session is taken over by another server. ```lua Profile.OnSessionEnd:Connect(function() print(`Profile session has ended - Profile.Data will no longer be saved to the DataStore`) end) ``` -------------------------------- ### Connect to OnCriticalToggle Signal Source: https://madstudioroblox.github.io/ProfileStore/api Connect to the OnCriticalToggle signal to be notified when the ProfileStore enters or exits a critical state. This can be used to inform players of potential service issues. ```lua ProfileStore.OnCriticalToggle:Connect(function(is_critical) if is_critical == true then print(`ProfileStore entered critical state`) else print(`ProfileStore critical state is over`) end end) ``` -------------------------------- ### Profile Object Properties Source: https://madstudioroblox.github.io/ProfileStore/api Overview of the properties available on a Profile object. ```APIDOC ## Profile Object Properties ### .Data (table) Player progress or data to be saved. Changes are saved if Profile:IsActive() is true. ### .LastSavedData (table, read-only) Version of .Data successfully stored in the DataStore. ### .FirstSessionTime (number, read-only) Unix timestamp of profile creation. ### .SessionLoadCount (number, read-only) Number of times a session has been started. ### .Session (table?, read-only) Contains PlaceId and JobId of the server where the session is active. ### .RobloxMetaData (table) Metadata saved with the DataStore key. Limited to 300 characters. ### .UserIds (table, read-only) List of associated user IDs. ### .KeyInfo (DataStoreKeyInfo) The DataStoreKeyInfo instance related to the profile. ### .OnSave (Signal) Fires immediately before changes to .Data are saved to the DataStore. ``` -------------------------------- ### ProfileStore Properties and Signals Source: https://madstudioroblox.github.io/ProfileStore/api This section covers read-only properties and signals exposed by the ProfileStore module that provide information about its state and events. ```APIDOC ## ProfileStore Properties and Signals ### .IsClosing ``` ProfileStore.IsClosing [bool] (read-only) ``` When the Roblox is shutting down this value will be set to `true` and most methods will silently fail. ### .IsCriticalState ``` ProfileStore.IsCriticalState [bool] (read-only) ``` After an excessive amount of DataStore calls fail this value will temporarily be set to `true` until the DataStore starts operating normally again. Might be useful for analytics or notifying players in-game of possible service disturbances. ### .OnError ``` ProfileStore.OnError [Signal] (message, store_name, profile_key) ``` A signal for DataStore error logging. Example: ```lua ProfileStore.OnError:Connect(function(error_message, store_name, profile_key) print(`DataStore error (Store:{store_name};Key:{profile_key}): {error_message}`) end) ``` ### .OnOverwrite ``` ProfileStore.OnOverwrite [Signal] (store_name, profile_key) ``` A signal for events when a DataStore key returns a value that has all or some of it's profile components set to invalid data types. E.g., accidentally setting `Profile.Data` to a non table value. Example: ```lua ProfileStore.OnOverwrite:Connect(function(store_name, profile_key) print(`Overwrite has occurred for Store:{store_name}, Key:{profile_key}`) end) ``` ### .OnCriticalToggle ``` ProfileStore.OnCriticalToggle [Signal] (is_critical) ``` A signal that is called whenever `ProfileStore.IsCriticalState` changes. Example: ```lua ProfileStore.OnCriticalToggle:Connect(function(is_critical) if is_critical == true then print(`ProfileStore entered critical state`) else print(`ProfileStore critical state is over`) end end) ``` ### .DataStoreState ``` ProfileStore.DataStoreState [string] "NotReady" | "NoInternet" | "NoAccess" | "Access" ``` Indicates ProfileStore's access to the DataStore. If at first check `ProfileStore.DataStoreState` is `"NotReady"`, it will eventually change to one of the other 3 possible values (`NoInternet`, `NoAccess` or `Access`) and never change again. `"Access"` means ProfileStore can write to the DataStore. ``` -------------------------------- ### ProfileStore Mocking for Testing Source: https://madstudioroblox.github.io/ProfileStore/api Utilize `ProfileStore.Mock` for testing purposes, allowing operations on a separate, non-persistent data store. ```APIDOC ## ProfileStore Mocking for Testing ### .Mock ```lua local PlayerStore = ProfileStore.New("PlayerData", {}) -- This profile would be saved to the DataStore: local LiveProfile = PlayerStore:StartSessionAsync("profile_key") LiveProfile.Data.Value = 1 LiveProfile:EndSession() -- This profile does not load data from the DataStore -- nor save data to the DataStore: -- (This data will disappear after the game server shuts down) local MockProfile = PlayerStore.Mock:StartSessionAsync("profile_key") MockProfile.Data.Value = 1 MockProfile:EndSession() ``` `ProfileStore.Mock` is a reflection of methods available in the `ProfileStore`, but said methods will now operate on profiles stored on a separate "fake" DataStore that will be forgotten when the game server shuts down. Profiles loaded using the same key from `ProfileStore` and `ProfileStore.Mock` will be different profiles because the regular and mock versions of a `ProfileStore` are isolated from each other. `ProfileStore.Mock` is useful for customizing your testing environment in cases where you want to enable Roblox API services in studio, but don't want ProfileStore to save to live keys: ```lua local RunService = game:GetService("RunService") local PlayerStore = ProfileStore.New("PlayerData", {}) if RunService:IsStudio() == true then PlayerStore = PlayerStore.Mock end ``` Notice Even when Roblox API services are unavailable, `ProfileStore` and `ProfileStore.Mock` will store profiles separately from each other. ``` -------------------------------- ### ProfileStore Name Property Source: https://madstudioroblox.github.io/ProfileStore/api Access the read-only name of the DataStore defined during initialization. ```lua ProfileStore.Name [string] (read-only) ``` -------------------------------- ### Messaging and Versioning Source: https://madstudioroblox.github.io/ProfileStore/datause Advanced methods for cross-server messaging and version history navigation. ```APIDOC ## :MessageAsync() ### Description Sends a message to a profile session, potentially across different servers. ### Method POST --- ## VersionQuery:NextAsync() ### Description Navigates through the version history of a profile. ### Method GET ``` -------------------------------- ### Profile:SetAsync() Source: https://madstudioroblox.github.io/ProfileStore/api Saves the profile data to the DataStore, disregarding active sessions. ```APIDOC ## Profile:SetAsync() ### Description Only works for profiles loaded through `ProfileStore:GetAsync()` or `ProfileStore:VersionQuery()`. Saves `Profile.Data` of a profile loaded with `ProfileStore:GetAsync()` to the DataStore disregarding any active sessions. ### Method `Profile:SetAsync()` ### Endpoint N/A ### Parameters This method does not take any parameters. ### Request Example ```lua Profile:SetAsync() ``` ### Response This method does not return a value. ``` -------------------------------- ### Study data mutation over time Source: https://madstudioroblox.github.io/ProfileStore/api Iterate through profile snapshots within a specific time range to analyze data changes. ```lua -- You have ProfileStore working in your game. You join -- the game with your own account and go to https://www.unixtimestamp.com -- and save the current UNIX timestamp resembling present time. -- You can then make the game alter your data by giving you -- currency, items, experience, etc. local PlayerStore = ProfileStore.New("PlayerData", {}) -- UNIX timestamp you saved: local min_date = DateTime.fromUnixTimestamp(1628952101) local print_minutes = 60 * 12 -- Print the next 12 hours of history local query = PlayerStore:VersionQuery( "Player_2312310", Enum.SortDirection.Ascending, min_date ) -- You can now attempt to print out every snapshot of your data saved -- at an average periodic interval of 60 minutes (Roblox DataStore caching interval) -- starting from the time you took the UNIX timestamp! local finish_update_time = min_date.UnixTimestampMillis + (print_minutes * 60000) print(`Fetching {print_minutes} minutes of saves:`) local entry_count = 0 while true do entry_count +=1 local profile = query:NextAsync() if profile ~= nil then if profile.KeyInfo.UpdatedTime > finish_update_time then if entry_count == 1 then print(`No entries found in set time period. (Start timestamp too early)`) else print(`Time period finished.`) end break end print(`Entry {entry_count} - {DateTime.fromUnixTimestampMillis(profile.KeyInfo.UpdatedTime):ToIsoDate()}`) print(profile.Data) -- Printing table for studio expressive output else if entry_count == 1 then print(`No entries found in set time period. (Start timestamp too late)`) else print(`No more entries in query.`) end break end end ``` -------------------------------- ### Connect to Profile.OnLastSave Signal Source: https://madstudioroblox.github.io/ProfileStore/api Fires before Profile.Data is saved to the DataStore. Use this for final data adjustments or penalties before a save. ```lua Profile.OnLastSave:Connect(function(reason: "Manual" | "External" | "Shutdown") print(`Profile.Data is about to be saved to the DataStore for the last time; Reason: {reason}`) end) ``` -------------------------------- ### Implement PurchaseId Caching for MarketplaceService Source: https://madstudioroblox.github.io/ProfileStore/devproducts This implementation uses a cache of PurchaseId's within the profile data to ensure that product grants are persisted before confirming the receipt to Roblox. ```lua local Profiles: {[player]: typeof(PlayerStore:StartSessionAsync())} = {} -- See Tutorial > Basic Usage local PURCHASE_ID_CACHE_SIZE = 100 local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local ProductFunctions = {} ProductFunctions[456456] = function(receipt, player, profile) profile.Data.Cash += 100 -- No Profile:Save() is needed in here compared to the previous example end function PurchaseIdCheckAsync(profile, purchase_id, grant_product): Enum.ProductPurchaseDecision -- Waits until purchase_id is confirmed to be saved to the DataStore or the profile session has ended if profile:IsActive() == true then local purchase_id_cache = profile.Data.PurchaseIdCache if purchase_id_cache == nil then purchase_id_cache = {} profile.Data.PurchaseIdCache = purchase_id_cache end -- Granting product if not received: if table.find(purchase_id_cache, purchase_id) == nil then local success, result = pcall(grant_product) if success ~= true then warn(`Failed to process receipt:`, profile.Key, purchase_id, result) return Enum.ProductPurchaseDecision.NotProcessedYet end while #purchase_id_cache >= PURCHASE_ID_CACHE_SIZE do table.remove(purchase_id_cache, 1) end table.insert(purchase_id_cache, purchase_id) end -- Waiting until the purchase is confirmed to be saved to the DataStore: local function is_purchase_saved() local saved_cache = profile.LastSavedData.PurchaseIdCache return if saved_cache ~= nil then table.find(saved_cache, purchase_id) ~= nil else false end if is_purchase_saved() == true then return Enum.ProductPurchaseDecision.PurchaseGranted end while profile:IsActive() == true do local last_saved_data = profile.LastSavedData profile:Save() if profile.LastSavedData == last_saved_data then profile.OnAfterSave:Wait() end if is_purchase_saved() == true then return Enum.ProductPurchaseDecision.PurchaseGranted end if profile:IsActive() == true then task.wait(10) end end end return Enum.ProductPurchaseDecision.NotProcessedYet end local function ProcessReceipt(receipt_info) local player = Players:GetPlayerByUserId(receipt_info.PlayerId) if player ~= nil then local profile = Profiles[player] while profile == nil and player.Parent == Players do profile = Profiles[player] if profile ~= nil then break end task.wait() end if profile ~= nil then if ProductFunctions[receipt_info.ProductId] == nil then ``` -------------------------------- ### Profile Management Methods Source: https://madstudioroblox.github.io/ProfileStore/api Methods to manage the active state, data structure, and session lifecycle of a profile. ```APIDOC ## :IsActive() ### Description Returns a boolean indicating if changes to `Profile.Data` will be saved. ## :Reconcile() ### Description Fills in missing variables inside `Profile.Data` based on the template provided during `ProfileStore.New()`. ## :EndSession() ### Description Stops auto-saving and triggers a final save of `Profile.Data` to the DataStore. ## :AddUserId(user_id) ### Description Associates a `UserId` with the profile for GDPR compliance. ### Parameters - **user_id** (number) - Required - The user ID to associate with the profile. ``` -------------------------------- ### Access Profile Data Source: https://madstudioroblox.github.io/ProfileStore/api Profile.Data holds the player's progress or other savable data. Changes are guaranteed to save if made after checking Profile:IsActive() == true or before Profile.OnSessionEnd. Critical data should be stored immediately after checking without yielding. ```lua Profile.Data [table] ``` -------------------------------- ### ProfileStore.Name Source: https://madstudioroblox.github.io/ProfileStore/api The name of the DataStore used by ProfileStore. ```APIDOC ## ProfileStore.Name ### Description The name of the DataStore that was defined as the first argument of `ProfileStore.New()`. ### Type `string` (read-only) ``` -------------------------------- ### Save Profile Data (Disregarding Sessions) Source: https://madstudioroblox.github.io/ProfileStore/api Saves Profile.Data for a profile loaded via GetAsync() or VersionQuery() to the DataStore, ending any active sessions for that profile. This method is intended for profiles not currently in an active session. ```lua Profile:SetAsync() ``` -------------------------------- ### Data Persistence Methods Source: https://madstudioroblox.github.io/ProfileStore/datause Methods for direct data manipulation and retrieval within the DataStore. ```APIDOC ## :GetAsync() ### Description Retrieves profile data from the DataStore. ### Method GET ### Parameters - None ### Response - **Success**: Returns the profile data object. --- ## :Save() ### Description Manually triggers a save of the profile data to the DataStore. ### Method POST ### Parameters - None --- ## :RemoveAsync() ### Description Removes the profile data from the DataStore. ### Method DELETE ### Parameters - None ``` -------------------------------- ### Connect to OnError Signal Source: https://madstudioroblox.github.io/ProfileStore/api Connect to the OnError signal to log DataStore errors. This is useful for monitoring service disturbances. ```lua ProfileStore.OnError:Connect(function(error_message, store_name, profile_key) print(`DataStore error (Store:{store_name};Key:{profile_key}): {error_message}`) end) ``` -------------------------------- ### MessageAsync Method Source: https://madstudioroblox.github.io/ProfileStore/api Sends a message to a profile regardless of active session status. Use sparingly as it consumes multiple UpdateAsync calls. ```lua ProfileStore:MessageAsync(profile_key, message) --> is_success [bool] -- profile_key [string] -- DataStore key -- message [table] -- Data to be stored in the profile before it's received ``` -------------------------------- ### Profile:MessageHandler() Source: https://madstudioroblox.github.io/ProfileStore/api Sets a function to handle incoming messages for the profile. ```APIDOC ## Profile:MessageHandler() ### Description Sets a function that will handle existing and future incoming messages sent to this profile by `ProfileStore:MessageAsync()`. ### Method `Profile:MessageHandler(function(message, processed)) ### Parameters #### Path Parameters - **message** (table) - Required - The message content. - **processed** (function) - Required - A function that must be called to acknowledge message processing. ### Request Example ```lua Profile:MessageHandler(function(message, processed) print(`Message received:`, message) processed() end) ``` ### Response This method does not return a value. ``` -------------------------------- ### ProfileStore:MessageAsync() Source: https://madstudioroblox.github.io/ProfileStore/api Sends a message to a profile, useful for critical data like in-game gifts, even if the profile is not currently active. ```APIDOC ## ProfileStore:MessageAsync() ### Description Sends a message to a profile, regardless of whether a session is active. Each call uses two `:UpdateAsync()` calls. This method is intended for critical data like gifting paid items to friends who may not be online. For less critical messages, consider using `MessagingService`. ### Method `ProfileStore:MessageAsync(profile_key, message)` ### Parameters #### Path Parameters - **profile_key** (string) - The DataStore key for the profile. - **message** (table) - The data to be stored in the profile before it is received. ### Response - **is_success** (boolean) - `true` if the message was sent successfully, `false` otherwise. ``` -------------------------------- ### Convert UserData to Table Source: https://madstudioroblox.github.io/ProfileStore/troubleshooting UserData types like Vector3 cannot be directly saved. Convert them to a table of numbers (e.g., X, Y, Z components) before saving. ```lua Profile.Data = {LastPosition = {position.X, position.Y, position.Z} } ``` -------------------------------- ### Handle Incoming Messages Source: https://madstudioroblox.github.io/ProfileStore/api Sets a function to handle messages sent to the profile. The provided function must call 'processed()' to acknowledge message handling. If not processed, messages may be re-broadcast. ```lua Profile:MessageHandler(function(message, processed) print(`Message received:`, message) processed() end) ``` -------------------------------- ### Check for NaN values Source: https://madstudioroblox.github.io/ProfileStore/troubleshooting Use this comparison to detect NaN values, which result from division by zero or certain math operations. NaN values cannot be saved by DataStore. ```lua print(NaN == NaN) --> false ``` -------------------------------- ### Remove Data with :RemoveAsync() Source: https://madstudioroblox.github.io/ProfileStore/api Use :RemoveAsync() to erase data from the DataStore. In live Roblox servers, this must be used on profiles created through ProfileStore.Mock after Profile:EndSession() and when the Profile will no longer be loaded. ```lua ProfileStore:RemoveAsync(profile_key) --> is_success [bool] -- profile_key [string] -- DataStore key ``` -------------------------------- ### Profile Session End Notification Source: https://madstudioroblox.github.io/ProfileStore/troubleshooting Connect to the OnSessionEnd event to receive a notification when a profile's session ends and its data will no longer be saved to the DataStore. This helps diagnose issues with timely session termination. ```lua Profile.OnSessionEnd:Connect(function() print(`Profile session has ended ({Profile.Key}) - Profile.Data will no longer be saved to the DataStore`) end) ``` -------------------------------- ### Connect to OnSave Signal Source: https://madstudioroblox.github.io/ProfileStore/api Profile.OnSave is a signal fired just before Profile.Data is saved. Changes to Profile.Data are expected to save if done at this moment, but this guarantee is lost after yielding. The signal fires before auto-saves, manual saves via Profile:Save(), and final saves after a session ends. ```lua Profile.OnSave:Connect(function() print(`Profile.Data is about to be saved to the DataStore`) end) ``` -------------------------------- ### Profile Lifecycle Signals Source: https://madstudioroblox.github.io/ProfileStore/api Signals that track the state of a profile session, including saving and termination events. ```APIDOC ## .OnLastSave ### Description A signal fired right before changes to `Profile.Data` are saved to the DataStore for the last time. It provides a reason for the session end: "Manual", "External", or "Shutdown". ## .OnSessionEnd ### Description Fired after a session has ended and no further changes to `Profile.Data` should be made. This signal fires even if a session is stolen. ## .OnAfterSave ### Description Fired every time after profile data has been successfully saved via `GlobalDataStore:UpdateAsync()`. Provides the `last_saved_data` as an argument. ``` -------------------------------- ### Access Last Saved Data Source: https://madstudioroblox.github.io/ProfileStore/api Profile.LastSavedData provides a read-only version of Profile.Data that was successfully stored. Useful for verifying saved data or handling developer product purchases securely. ```lua Profile.LastSavedData [table] (read-only) ``` -------------------------------- ### End Profile Session and Save Data Source: https://madstudioroblox.github.io/ProfileStore/api Stops auto-saving for a profile and triggers a final save. Call this when you are finished with a profile object, such as when a player leaves. ```lua Players.PlayerRemoving:Connect(function(player) local profile = Profiles[player] if profile ~= nil then profile:EndSession() Profiles[player] = nil end end) ``` -------------------------------- ### Access Roblox Metadata Source: https://madstudioroblox.github.io/ProfileStore/api Profile.RobloxMetaData is a table saved as metadata for the DataStore key. Be cautious of harsh limits; total table content size cannot exceed 300 characters. Changes are saved on the next auto-save or when the profile session ends. ```lua Profile.RobloxMetaData [table] ``` -------------------------------- ### Add UserId to Profile Source: https://madstudioroblox.github.io/ProfileStore/api Associates a UserId with a profile, useful for GDPR compliance and managing profiles shared by multiple users. ```lua Profile:AddUserId(user_id) ``` -------------------------------- ### Access User IDs Source: https://madstudioroblox.github.io/ProfileStore/api Profile.UserIds is a read-only table containing user IDs associated with the profile. Entries must be added with Profile:AddUserId() and removed with Profile:RemoveUserId(). ```lua Profile.UserIds [table] (read-only) -- {user_id [number], ...} ``` -------------------------------- ### ProfileStore:RemoveAsync Source: https://madstudioroblox.github.io/ProfileStore/api Removes a profile from the DataStore. ```APIDOC ## RemoveAsync(profile_key) ### Description Erases data from the DataStore. In live Roblox servers, this must be used on profiles created through ProfileStore.Mock after Profile:EndSession() and when the profile will no longer be loaded. ### Parameters #### Path Parameters - **profile_key** (string) - Required - The DataStore key to remove. ### Response - **is_success** (boolean) - Returns true if the operation was successful. ``` -------------------------------- ### Remove User ID from Profile Source: https://madstudioroblox.github.io/ProfileStore/api Unassociates a UserId with the profile. Use this when a user should no longer be linked to a specific profile. ```lua Profile:RemoveUserId(user_id) -- user_id [number] ``` -------------------------------- ### Profile:RemoveUserId() Source: https://madstudioroblox.github.io/ProfileStore/api Unassociates a UserId with the profile. ```APIDOC ## Profile:RemoveUserId() ### Description Unassociates a `UserId` with the profile. ### Method `Profile:RemoveUserId(user_id)` ### Parameters #### Path Parameters - **user_id** (number) - Required - The ID of the user to remove. ### Request Example ```lua Profile:RemoveUserId(123456789) ``` ### Response This method does not return a value. ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.