Okay, so heres the deal. I have done an insane amount of researching on Dev Forums, Google, etc. Tons of websites/forums. There is not a clear answer on best practices for placing player stats. I would like to get a good answer and share with the world.
I have heard replicated storage is the place to put stats, however it can still be accessed by the client so how is this safe?
I have heard to just user player, but obviously easy to exploit.
I have heard server storage, which also heard is safe but super inefficient amongst other things.
The reason you probably are getting alot of confusion in this part, is because you haven’t viewed Roblox’s Server <-> Client Model. Any changes made by a client through localscripts, in client side isn’t visible to the server / any other client unless its a change made Via a Remote event in the server. This is what Filtering Enabled is basically for.
But the best place to store player stats, imo is inside the Player, you can create a folder and place the stats etc in there. The reason I choose player instance to store the stats is because:
I don’t need to create another folder for that person and remove it when player leaves
Both server and client can read from it
Also its not easy to exploit the player instance’s child value etc unless you have easily exploitable Remote events. Therefore you should change all player stats/update player stats on Server side Only. If this is done on the client side, then yes its easy.
Okay, I do understand the filtering enabled part. However, I would prefer to use player, it is much easier.
What do you mean by easily exploitable remote events? how would you make a remote event non exploitable? I have a game built and currently integrating data store. In order for the data store to save, I have remote events set up to update the values on the server anyways. I have tons of player “stats” would it be considered normal to have remote events for all of these stats then?
Does what I said even make sense? lol - I am a noob.
DataStore code - take a look at the format for adding stats/having stats
local replicatedStorage = game:GetService("ReplicatedStorage")
-- GET DATA STORE SERVICE--
local DataStoreService = game:GetService("DataStoreService")
--------------------------------------------
--DATA STORES--
local GoldStore = DataStoreService:GetDataStore("PlayerGold")
local LevelStore = DataStoreService:GetDataStore("PlayerLevel")
local ExperienceStore = DataStoreService:GetDataStore("PlayerExperience")
local maxExpStore = DataStoreService:GetDataStore("MaxExperience")
--------------------------------------------
-- ON PLAYER ADDED --
game.Players.PlayerAdded:Connect(function(player)
--DATA VARIABLES SET AS NIL VALUE FOR NOW, GET UPDATED BELOW --
local GoldCoinsData
local LevelData
local ExpData
local maxExpData
-- LEAVE THE STAT VARIABLES AS GLOBAL (NON LOCAL VARIABLES) SO WHOLE SCRIPT CAN USE --
-- LEADERSTATS --
leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
-- ADD LEADERSTATS BELOW --
GoldCoins = Instance.new("IntValue")
GoldCoins.Name = "GoldCoins"
GoldCoins.Parent = leaderstats
GoldCoins.Value = 0
Level = Instance.new("IntValue")
Level.Name = "Level"
Level.Parent = leaderstats
Level.Value = 1
-- PLAYER STATS --
playerStats = Instance.new("Folder")
playerStats.Name = "playerStats"
playerStats.Parent = player
-- ADD PLAYER STATS BELOW --
Experience = Instance.new("IntValue")
Experience.Name = "Experience"
Experience.Value = 0
Experience.Parent = playerStats
maxExperience = Instance.new("IntValue")
maxExperience.Name = "maxExperience"
maxExperience.Value = 1100
maxExperience.Parent = playerStats
--------------------------------------------
-- RESOURCES --
resourceStats = Instance.new("Folder")
resourceStats.Name = "resourceStats"
resourceStats.Parent = player
-- ADD RESOURCES BELOW --
--------------------------------------------
-- ITEMS --
itemStats = Instance.new("Folder")
itemStats.Name = "itemStats"
itemStats.Parent = player
-- ADD ITEMS BELOW --
-------------------------------------------
local success, errormessage = pcall(function()
GoldCoinsData = GoldStore:GetAsync(player.UserId.."-GoldCoins")
LevelData = LevelStore:GetAsync(player.UserId.."-Level")
ExpData = ExperienceStore:GetAsync(player.UserId.."-Experience")
maxExpData = maxExpStore:GetAsync(player.UserId.."-maxExperience")
end)
if success then -- if success then sets a the value to DATA variable
GoldCoins.Value = GoldCoinsData
Experience.Value = ExpData
if LevelData then
Level.Value = LevelData
end
if maxExpData then
maxExperience.Value = maxExpData
end
else
warn(errormessage) -- if fails then gives error
end
end)
-- ON PLAYER REMOVE --
game.Players.PlayerRemoving:Connect(function(player)
local success, errormessage = pcall(function()
GoldStore:SetAsync(player.UserId.."-GoldCoins", player.leaderstats.GoldCoins.Value)
LevelStore:SetAsync(player.UserId.."-Level", player.leaderstats.Level.Value)
ExperienceStore:SetAsync(player.UserId.."-Experience", player.playerStats.Experience.Value)
maxExpStore = maxExpStore:GetAsync(player.UserId.."-maxExperience")
end)
if success then -- if success then the data gets saved via SetAsync above
print("DATA SAVED")
else
warn(errormessage) -- if fails then gives error
end
end)
-- LOOP ON PLAYER ADDED TO SAVE DATA EVERY 120 SECONDS)
game.Players.PlayerAdded:Connect(function(player)
while true do
wait(300) -- SAVE INTERVAL (SAFTEY IN CASE SERVER CRASHES, ETC)
local success, errormessage = pcall(function()
GoldStore:SetAsync(player.UserId.."-GoldCoins", player.leaderstats.GoldCoins.Value)
LevelStore:SetAsync(player.UserId.."-Level", player.leaderstats.Level.Value)
ExperienceStore:SetAsync(player.UserId.."-Experience", player.playerStats.Experience.Value)
maxExpStore = maxExpStore:GetAsync(player.UserId.."-maxExperience")
end)
if success then -- if success then the data gets saved via SetAsync above
print("DATA SAVED")
else
warn(errormessage) -- if fails then gives error
end
end
end)
With easily exploitable Remote events, I mean like for example take this scenario, Player1 wants to buy Item1 for Shop, which costs 500 Coins, and Player1 currently has 300.
So, if your code is like this:
--Client
local buyItemRemote = <pathToRemote>
buyItemRemote:FireServer("Apple", player.leaderstats.Coins.Value) --Exploiter can just change this to 10000000 or anything.
--Server
local buyItemRemote = <pathToRemote>
buyItemRemote.OnServerEvent:Connect(function(player, item, coins)
--Here you use logic and check if the coins are more than needed coins for buying that item
end)
Although this seems good, its really easy for an exploiter to bypass and get millions of coins. Because the client has total access to how they want to pass data to Remote events and if they pass, say 1000000 coins in the Coins argument, then the server would check it and give player the item.
Instead the correct way to do this is check coins on the server side, by matching it with the player’s leaderstat coin value.
I can see that your Remote events script is actually exploitable, if I am right, as you’re doing exactly similar to what I have explained above, instead you should do it on the Server side I believe.
About the Datastore script, I haven’t have time to go totally over it, but some recommendations:
Try to combine all player data to one datastore key for the player, instead of having multiple datastores for each stat
Use BindToClose and playerRemoving event instead of that while true do loop for auto saving data.
I am not sure how you would do it with your scripts, as I don’t have total knowledge of the game but I can surely give the example of doing it correctly for the example I gave above that is exploitable.
--Client
local buyItemRemote = <pathToRemote>
buyItemRemote:FireServer("Apple")
--Server
local buyItemRemote = <pathToRemote>
local itemData = { Apple = {Cost = 500} }
buyItemRemote.OnServerEvent:Connect(function(player, item)
if player.leaderstats.Coins.Value >= itemData[item].Cost then
--However you wanna give item to player
end
end)
This can’t be bypassed as a fact that you’re checking it in the server side, which isn’t accessible by the players.
You should also have some simple checks like:
buyItemRemote.OnServerEvent:Connect(function(player, item)
if not itemData[item] then return end --Checks if item exists in your data, if not then end the function.
if player.leaderstats.Coins.Value >= itemData[item].Cost then
--However you wanna give item to player
end
end)
Honestly up to you in my opinion, you can store player values in ServerStorage (for only the server to see), ReplicatedStorage (for it to be shared among the players and the server), or you can create a ModuleScript with the code
return {};
to return an empty module where you can store player data as tables. If you’re working on a large project though, I’d suggest the last option, as you’d be easily able to store and change around player information and make it clearly organized with tables. Hope this helps!