Save your player data with ProfileService! (DataStore Module)

[12/18/2020] - The good stuff update!

ProfileService module no longer yields when calling require(ProfileService)!
Grab your update the easy way or the cool way. :dark_sunglasses:


Added Rojo support.


ProfileService.IssueSignal now provides profile_store_name and profile_key parameters so you wouldn’t be left in the dark.

11 Likes

Tables cannot be cyclic?

 -- Check how many if it's beyond 4 million then probably reset it
        local dataCounter = HttpService:JSONEncode(PlayerProfile)
        print(Player.Name.." has "..string.len(dataCounter).." in their Data")
-- Sample Data
local SamplePlayerData = {
    Stats = {
        Coins = 0;
        Level = 1;
        EXP = 0;
        MaxEXP = 0;
        Quests = 0;
        MaxQuests = 10;
        Clan = "none"; -- defines if {Mage, Warrior, Tank}
        Physical = 0;
        Spell = 0;
        Stamina = 0;
        Banned = false;
    };

    Inventory = {
        -- Weapons
        -- Potions

        --[[
            Weapon = {
                Spell = 0;
                Physical = 0;
                Delay = 1;
                Class = 0;
                Rarity = "Rare";
                Tradeable = false;
                Upgrades = 0;
                MaxUpgrades = 99;
                SellAmount = 0;
            }
        ]]

        --[[
            Potion = {
                Rarity = "Mythical";
                Drinked = false;
                Class = "none";
                SellAmount = 0;
                Tradeable = false;
            }
        ]]

        -- Armors
        -- Helmet
        -- Chestplate
        -- Pants

        --[[
            Helmet = {
                Rarity = "none";
                Class = "none";
                SellAmount = 0;
                Tradeable = false;
                Spell = 0;
                Physical = 0;
                Stamina = 0;
            }
        ]]

        --[[
            Armor = {
                Rarity = "none";
                Class = "none";
                SellAmount = 0;
                Tradeable = false;
                Spell = 0;
                Physical = 0;
                Stamina = 0;
            }
        ]]

        --[[
            Pant = {
                Rarity = "none";
                Class = "none";
                SellAmount = 0;
                Tradeable = false;
                Spell = 0;
                Physical = 0;
                Stamina = 0;
            }
        ]]

        Tools = {
            Weapons = {

            };

            Potions = {

            };
        };

        Armors = {
            Helmets = {

            };

            Chestplates = {

            };

            Pants = {

            }
        };

        Spells = {

        }
    };

    Equipment = {
        Helmet = "none";
        Chestplate = "none";
        Pants = "none";
        Sword = "none";
        Sword1 = "none";
        Spell = "none";
        Spell1 = "none";
    };
}

Ouput

  tables cannot be cyclic  -  Server  -  StatsService:194
  14:19:27.223  Stack Begin  -  Studio
  14:19:27.223  Script 'ServerStorage.Aero.Services.GameStats.StatsService', Line 194 - function PlayerAdded  -  Studio  -  StatsService:194
  14:19:27.223  Stack End  -  Studio

what?

Output

ServerStorage.Aero.Services.GameStats.StatsService:151: attempt to index nil with 'Data'  -  Server  -  StatsService:151
  14:22:07.819  Stack Begin  -  Studio
  14:22:07.819  Script 'ServerStorage.Aero.Services.GameStats.StatsService', Line 151 - function KickPlayer  -  Studio  -  StatsService:151
  14:22:07.819  Script 'ServerStorage.Aero.Services.GameStats.StatsService', Line 189 - function PlayerAdded  -  Studio  -  StatsService:189
  14:22:07.819  Stack End  -  Studio

Code

local function KickPlayer(Player, PlayerProfile)
        if Banned[tostring(Player.Name)] then
            Player:Kick("You were banned automatically!")
        elseif PlayerProfile.Data.Stats.Banned == true then
            Player:Kick("You were banned by an Admin!")
        else
            print(Player.Name.." is allowed in the game!")
        end
    end

Caller of Function

local function PlayerAdded(Player)
        local PlayerProfile = GameProfileStore:LoadProfileAsync(
            "Player_"..Player.UserId,
            "ForceLoad"
        )

        -- Baically Preventing Item loss and Duplicates or as Session Locking
        if PlayerProfile ~= nil then
            PlayerProfile:Reconcile() -- if there is something missing then fill it up

            PlayerProfile:ListenToRelease(function()
                -- might have loaded on another server
                Profiles[Player] = nil

                Player:Kick()
            end)
            if Player:IsDescendantOf(Players) == true then
                -- woo hoo yes
                Profiles[Player] = PlayerProfile
                print("Data successfully Loaded!")
                KickPlayer(Player, PlayerProfile)
            else
                -- oops
                Player:Release()
            end
        else
            -- Couldn't load it
            Player:Kick()
        end

        -- Data Handling is done check if they are banned
        KickPlayer(Player)

        -- Check how many if it's beyond 4 million then probably reset it
        local dataCounter = HttpService:JSONEncode(PlayerProfile)
        print(Player.Name.." has "..string.len(dataCounter).." in their Data")
    end
1 Like

so HttpService:JSONEncode(Profile.Data ?

The Profile object is cyclic because it has a reference to a ProfileStore object that has a reference back to the Profile. On the other hand, Profile.Data is exactly what you set it to be and is not even allowed to be cyclic, so you can JSON encode it.

Great, that solves my problem the new problem is that is there an onUpdate() function similar to DataStore2 ?

You will have to implement that yourself. One way to do it would be to have a container class for Profiles that has setter methods. Alternatively, you could use loleris’ new ReplicaService, which kind of builds that in for you.

but does it update also when I change some data?

If you mean the DataStore, no. ProfileService autosaves around every 90 seconds by default I believe, so changing data won’t immediately save.

I’m not saying anything about auto-save and its basically like this: change coins to 90 from 30 > does it fire when it updates like that?

Are you referring to this? Yes, your implementation could have a custom signal that fires whenever a setter method is called.

ProfileService doesn’t have an Set() method, alternatively if values like this Name = Value have an .Changed() then it basically solves all my problems or even better aa function that detects whether an table is updated!

…Please read my original reply. I believe I already answered that.
Anyways, personally, I believe that a custom change signal is more useful. Using Rodux as an example, it has a changed signal that simply gives you the old state and new state, making you check against old values to find differences. Although this isn’t necessarily a downside, custom signals for changes are more readable as opposed to a one-size-fits-all change signal.

then i’d like some example or guide for this then…

If you’re comfortable with making Lua-style classes, you could create a pretty simple solution easily. In the constructor, give it the Profile and create the signals you need (eg: CashChanged, etc.). Then, just add setter methods that set the value and fire the appropriate signal.

Hey,

I noticed that the data takes over 25 seconds (roughly 30) to load. I don’t have a wait on the releases, but I do realize that the following is printed:

[ProfileService]: DataStore API error (Store:“PlayerData”;Key:“Player_81553363”) - “ServerScriptService.dataRetriever.ProfileService:548: attempt to index nil with ‘UpdateAsync’”

I also realize it gets stuck when I run LoadProfileAsync
Recommendations?

Thanks

Could you show your code that calls LoadProfileAsync?

Sure:

local profile = GameProfileStore:LoadProfileAsync(playerID, “ForceLoad”)

^ where player is “Player_”…player.UserId

This is the really basic implementation of it using ForceLoad, I tried with Steal but it is the same. Noticed sometimes it is fast as lightning then occasionally it is taking a moment. I thought it had something to do with the release but then I ruled that out because leaving causes the release, without any wait.

30 second load is usually the cause of not releasing. You have to be 120% sure your code gets to the point where it releases the profile, every time. Do prints and do some offline server testing.

2 Likes

[12/20/2020] - Studio bug fix (Online mode is not vulnerable to this)

Fixed a race condition where a profile could be loaded before the live key access check finishes - this bug was introduced with the no yield on require update two days ago. This can be the cause of errors in studio testing.

Update your module here:
Roblox library
GitHub

2 Likes

Quick question - is it possible to bind functions to some kind of event that occurs when data is changed, such as with DataStore2’s :OnUpdate callback?

Just getting into ProfileService and loving the flexibility and scalability!