Yeah! Nice job on figuring it out lol XD - This change enables yielding for the MadworkScriptSignal system
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
- Refer to my method here, although I’m sure this isn’t the best method.
- No it doesn’t, but you could implement your own! Also, Luau has compound operators!
- 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.
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
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.
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?
Really cool! I just found out about ProfileService and ReplicaService after looking for clean open source solutions for data in new games. Why re-invent the wheel when someone else has done the work for you? I’ll definitely be using these. Thanks!
Is the Profile.Data
saved in a single key or multiple keys? Because what if I reach the max data store limit, does this automatically distributes it over to several keys, or data fails to save?
Asking this because in my game there will be an inventory system with no limit of the number of items, each with their own properties saved, and I am not planning to add any sort of compression on the data that will be saved. Do I need to worry of it reaching the limit or is it too high that no one will reach it?
ProfileService uses a single DataStore key - the limit for storing under one profile is less than 4 megabytes. 4 megs is a heck ton of data, but you should make minimal estimates on how much data you’re going to take up. Consider stacking data that can be stacked and not make small redundant differences in objects that are still more or less the same.
Using the ProfileService, it’s giving me an error through the Service’s line of code
13:53:44.591 BindToClose can only be called on the server. - Client - Profile Service:1511
13:53:44.591 Stack Begin - Studio
13:53:44.591 Script 'ReplicatedStorage.Modules.Profiles.Profile Service', Line 1511 - Studio - Profile Service:1511
13:53:44.592 Stack End - Studio
Here’s the line of code, if needed.
game:BindToClose(function()
if UseMockDataStore == true then
return -- Ignores OnCloseTasks if ProfileService is running on MockDataStore
end
ProfileService.ServiceLocked = true
-- 1) Release all active profiles: --
-- Clone AutoSaveList to a new table because AutoSaveList changes when profiles are released:
local on_close_save_job_count = 0
local active_profiles = {}
for index, profile in ipairs(AutoSaveList) do
active_profiles[index] = profile
end
-- Release the profiles; Releasing profiles can trigger listeners that release other profiles, so check active state:
for _, profile in ipairs(active_profiles) do
local is_active = profile._profile_store._loaded_profiles[profile._id] == profile
if is_active then
on_close_save_job_count = on_close_save_job_count + 1
coroutine.wrap(function() -- Save profile on new thread
SaveProfileAsync(profile, true)
on_close_save_job_count = on_close_save_job_count - 1
end)()
end
end
-- 2) Yield until all active profile jobs are finished: --
while on_close_save_job_count > 0 or ActiveProfileLoadJobs > 0 or ActiveProfileSaveJobs > 0 do
RunService.Heartbeat:Wait()
end
return -- We're done!
end)
Is it because im calling the profile service in a local script or something? Can’t seem to find out what’s the cause of this.
This module doesn’t work on clients because DataStores are server-only.
Is there a way I can detect if a player has just joined for the first time/has no data?
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!