Save your player data with ProfileService! (DataStore Module)

This module doesn’t work on clients because DataStores are server-only.

2 Likes

Is there a way I can detect if a player has just joined for the first time/has no data?

1 Like

This is brilliant, im reading through your API right now and im already seeing so many areas where this can come in handy for modern games on Roblox… Blown away at how efficient it is, considering what its doing. Well done mate, very impressive, im for sure using this, thanks!

Is GlobalUpdates a good choice for making a messaging system with saving(as global updates are saved automatically) ?

If you are trying to use it as a real-time messaging system, no, since updates could take up to a full autosave cycle to be detected.

3 Likes

Is there anyway i can detect if the table Profile.Data is changed?

I can use metamethod __newindex. Does setting a metamethod to the table will mess-up anything?

2 Likes

Yes, it will mess up things. A better way would be to put the Profile in a wrapper class or something and have setter methods that trigger side effects.

2 Likes

Shutdown handling is included. I encourage exploring the source code :slight_smile:

3 Likes

[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.