Save your player data with ProfileService! (DataStore Module)

ProfileServices handles a lot of things so that you don’t have to think about them. Assuming you already have proper error handling (which ProfileService does for you), when you use GetAsync and UpdateAsync normally, your data is still sometimes susceptible to being overwritten such as when a player joins and leaves quickly.

Since ProfileService doesn’t use DataStore2’s OrderedBackups method, transferring should be easy. However, you should note that you will have to convert old player data to another format (ProfileService stores some metadata along with your actual data).

As for global leaderboards, I’m not sure what the best method is, but right now I simply use a FastSpawn (or spawn/coroutine) in my :ListenToRelease() connection that saves data to a global leaderboard.

1 Like

How will I transfer the data though? Is there a function for it?

Assuming your data format can be stored in a single table, all you’d have to do is:

local your_data -- [table]
Profile.Data = your_data

ProfileService does not have a “format”, but you simply can’t set Profile.Data itself to anything other than a table. Obviously, you can store (mostly) whatever you like inside a table.

Try surfing the documentation and hopefully you’ll get a better understanding of the ProfileService workflow.

1 Like

Go through the troubleshooting manual. If your code does not have faults noted in troubleshooting, you can DM me your data store script source which you can upload to something like pastebin.

I am getting error “tables cannot be cyclic”, how do I fix.

Code (this is not the full code). The code is inside a module script called Data Manager which is parent of profile service module:

local function PlayerAdded(Plr)
	local PlayerProfile = ProfileStores.PlayerData:LoadProfileAsync(
		"Plr_"..Plr.UserId,
		"ForceLoad"
	)
	if (not PlayerProfile) then
		Plr:Kick("Data failed to load.")
	else
		PlayerProfile:Reconcile()
		PlayerProfile:ListenToRelease(function()
			Profiles.PlayerData[Plr] = nil
			Plr:Kick()
		end)
		if (Plr:IsDescendantOf(Players)) then
			Profiles.PlayerData[Plr] = PlayerProfile
			Bindables.OnPlayerProfileLoaded:Fire(Plr,PlayerProfile) -- Error is here
		else
			PlayerProfile:Release()
		end
	end
end

Leaderstats (a different script):

DataManager:OnPlayerProfileLoaded(function(Plr,Profile) -- This listens to the bindable event fired
	local Wins = Plr:WaitForChild("leaderstats"):WaitForChild("Wins")
	Wins.Value = Profile.Data.Wins
end)

If you want the full scripts I can private message them to you.

Edit: This only seems to be happening when I pass the profile as an argument. If I pass for e.g an empty table it works. How do I fix this?

BindableEvents can’t send live references to objects (objects are just tables with data). A BindableEvent will attempt to make a copy of a table, but, in the case of copying a Profile object, it will fail due to circular references being used in the ProfileService system.

Consider using a custom signal module (instead of BindableEvents) like MadworkSignal which can pass live references - It’s part of the ReplicaService package.

2 Likes

Does the signal module support yielding?

Edit: Replaced pointer(...) with coroutine.wrap(pointer)(...). I don’t think this will have any performance problems because the event will only fire when player joins.

Yeah! Nice job on figuring it out lol XD - This change enables yielding for the MadworkScriptSignal system

3 Likes

Just one question will this be compatible into making game leaderboards and uses API like GetOrderedDataStores() and does session locking actually prevent this type of issues, second, does it have functions like Increment() or Set() because it’s just so long to write profile.Data.Cash = profile.Data.Cash + numberHere and lastly, do you have a function that sets table’s or not or it just automatically just saves it when it is inserted? if not then I’d like an example from you

  1. Refer to my method here, although I’m sure this isn’t the best method.
  1. No it doesn’t, but you could implement your own! Also, Luau has compound operators!
  2. What do you mean by setting a table? Every time ProfileService saves, whatever data is in the Data field in the Profile is the data that is saved.

ProfileService is NOT for making leaderboards / creating ordered OrderedDaraStores. It’s not designed for it nor it would make it easy to do it. OrderedDaraStores don’t need nearly as many precautions (messing them up is rarely a tragedy) and are easy to implement on their own.

Instead of:
profile.Data.Cash = profile.Data.Cash + numberHere

Use:

local data = profile.Data
data.Cash += number_here

ProfileService keeps Profile.Data saved to the DataStore - you can put mostly any kind of values inside and it will save as long as Profile:IsActive() returns true or until the profile is released.

1 Like

Ok that is good but here is another issue isn’t Dictionaries Saveable into DataStores? because here is an issue right here:

 15:17:23.788  [ProfileService]: DataStore API error - "104: Cannot store Dictionary in data store. Data stores can only accept valid UTF-8 characters."

For the Sample Data Here it is:

local PlayerDataSample = {
    Stats = {
        Coins = 0;
        Level = 1;
        EXP = 0;
        MaxEXP = 50;
        Class = "Mage"; -- {Mage, Warrior, Tank}
        Quests = 0;
        MaxQuests = 5; -- maximum is 5
        Rank = "Rookie";
        DoubleSword = false;
        Banned = false;
        SpellPoints = 1;
        PhysicalPoints = 1;
        Stamina = 1;
    };

    SetUp = {
        Helmet = "none";
        Armor = "none";
        Pants = "none";
        Boots = "none";
        Sword = "none";
        DualSword = "none"
    };

    Equipment = {
        Armors = {
            Chestplates = {

            };

            Helmets = {

            };

            Pants = {

            };
        };
    };

    Tools = {
        Potions = {

        };

        Weapons = {
            ["Wooden Sword"] = {
                Path = game.ServerStorage.Items.Weapons.Warrior["Wooden Sword"];
                Damage = 1;
                ClassFor = "Warrior";
                Type = "Sword";
                Rarity = "Common";
                ChanceToGet = 100;
                Physical = 1;
                Spell = 1;
                Upgrades = 0;
                MaxUpgrades = 15;
                Debounce = false;
                CanDamage = false;
                IsTradeable = false;
            }
        };
    };

    Spells = {
        --[[
        ["SPELL_NAME"] = {
            Class = "Mage";
            BaseDamage = 0;
            LevelRequired = 0;
            SellAmount = 0;
            Rarity = "Common";
            ChanceToGet = 0;
            SellAmount = 0;
            IsTradeable = false;
        }
        ]]
    };

    Quests = {
        -- for examples only
        --[[
            ["QuestName"] = {
                IsCompleted = false;
                XPWorth = 0;
                CoinsWorth = 0;
                QuestDescription = "Kill 1 Thing";
                ThingsDone = 1;
                IsFromNPC = "nil"
            }
        ]]
    }
}

and the loading code the same code you gave but edited

-- Init profile service
    local GameProfileStore = ProfileService.GetProfileStore(
        "PlayerData",
        PlayerDataSample
    )
    
    local function Debug(profile)
        if profile ~= nil then
            print("Successfully loaded data!")
        else
            print("Failed to load Data")
        end
    end

    local function KickPlayer(Player)
        local PlayerData = GameProfileStore:LoadProfileAsync(
            "Player_" .. Player.UserId,
            "ForceLoad"
        )

        if PlayerData ~= nil then
            PlayerData:Reconcile() -- Fill in missing variables from ProfileTemplate (optional)
            PlayerData:ListenToRelease(function()
                Profiles[Player] = nil
                -- The profile could've been loaded on another Roblox server:
                Player:Kick()
            end)
            if Player:IsDescendantOf(Players) == true then
                Profiles[Player] = PlayerData
                -- A profile has been successfully loaded:
                Debug(PlayerData)
            else
                -- Player left before the profile loaded:
                PlayerData:Release()
            end
        else
            -- The profile couldn't be loaded possibly due to other
            --   Roblox servers trying to load this profile at the same time:
            Player:Kick() 
        end

        spawn(function()
            if BannedPlayers[tostring(Player.Name)] then
                Player:Kick("You are permanently Banned from this game, you are not welcome.")
            elseif PlayerData.Stats.Banned == true then
                Player:Kick("You are permanently Banned from this game, you are not welcome.")
            else
                print(Player.Name.." is authorized for this game!")
            end
        end) 
    end
    
    local function PlayerAdded(Player)
        print("Loading Data for "..Player.Name)

        local PlayerData = GameProfileStore:LoadProfileAsync(
            "Player_" .. Player.UserId,
            "ForceLoad"
        )

        if PlayerData ~= nil then
            PlayerData:Reconcile() -- Fill in missing variables from ProfileTemplate (optional)
            PlayerData:ListenToRelease(function()
                Profiles[Player] = nil
                -- The profile could've been loaded on another Roblox server:
                Player:Kick()
            end)
            if Player:IsDescendantOf(Players) == true then
                Profiles[Player] = PlayerData
                -- A profile has been successfully loaded:
                Debug(PlayerData)
            else
                -- Player left before the profile loaded:
                PlayerData:Release()
            end
        else
            -- The profile couldn't be loaded possibly due to other
            --   Roblox servers trying to load this profile at the same time:
            Player:Kick() 
        end

btw the last function without the end isn’t the bug the loading is

Saving same as the old code

local function PlayerRemoved(Player)
        local Backpack = Player.Backpack
        local profile = Profiles[Player]

        if profile ~= nil then
            profile:Release()
        end
    end
1 Like

Would be helpful if you read through the troubleshooting page first as it seems you’re trying to store Roblox instances which are not serializable (“Path” member).

so

game.ServerStorage.PathHere

makes the bug?

For leaderboards that means we save a duplicate of the data, like Data.Cash in a separate normal data store and use that for leaderboards?

Yeah, to create a leaderboard you update a snapshot of player’s score in an ordered datastore.

1 Like

Yes. It seems like you are saving too much to the DataStore for your items. Simply save the weapon’s identifier and any metadata, and store all of the extra stuff (Path,ClassFor,Type,ChanceToGet,MaxUpgrades,Debounce,CanDamage,IsTradeable) for items in a database.

I’m quite bad at reading codes, how can we make a global leaderboard with this? sorry if this has already been answered

This module is not for making leaderboards (normal datastore works fine for it) or even currency. This module used mainly to save things like inventory for example.

so I would have to use normal datastore for that?

1 Like