Since I haven’t been programming for that long, I decided to try create a datastore module for my own little project to improve on my skills. I don’t know if what I’ve done is of a decent standard so any feedback on how to improve it is appreciated.
local DS = {}
local DataStoreService = game:GetService("DataStoreService"):GetDataStore("A2")
function CREATE_PROFILE(player,DataTable)
local MainFolder = Instance.new("Folder",player)
MainFolder.Name = "Profile"
local Coins = Instance.new("IntValue",MainFolder)
Coins.Name = "Coins"
local GamepassesFolder = Instance.new("Folder",MainFolder)
GamepassesFolder.Name = "GamepassesFolder"
local sounds = Instance.new("Folder",MainFolder)
sounds.Name = "SoundsFolder"
local CurrentSound = Instance.new("StringValue",MainFolder)
CurrentSound.Name = "CurrentSound"
local RegionsFolder = Instance.new("Folder",MainFolder)
RegionsFolder.Name = "RegionsFolder"
local UIScaleValue = Instance.new("StringValue",MainFolder)
UIScaleValue.Name = "UIScaleValue"
local IsBannedValue = Instance.new("BoolValue",MainFolder)
IsBannedValue.Name = "IsBanned"
if DataTable == nil then
print("New Player")
Coins.Value = 0
CurrentSound.Value = "Basic Breathe"
IsBannedValue.Value = false
UIScaleValue.Value = "1"
local region = Instance.new("Folder",RegionsFolder)
region.Name = "Forest"
local breathe = Instance.new("Folder",sounds)
breathe.Name = "Basic Breathe"
else
Coins.Value = DataTable[1]
CurrentSound.Value = DataTable[2]
IsBannedValue.Value = DataTable[3]
UIScaleValue.Value = DataTable[4]
for i = 1,#DataTable[5],1 do
local Item = Instance.new("Folder",RegionsFolder)
Item.Name = DataTable[5][i]
end
for i = 1,#DataTable[6],1 do
local Item = Instance.new("Folder",sounds)
Item.Name = DataTable[6][i]
end
end
end
local function GET_VALUES(player)
local profile = player:FindFirstChild("Profile")
local values = {}
if profile then
table.insert(values,profile:WaitForChild("Coins").Value)
table.insert(values,profile:WaitForChild("CurrentSound").Value)
table.insert(values,profile:WaitForChild("IsBanned").Value)
table.insert(values,profile:WaitForChild("UIScaleValue").Value)
local regions = {}
for i,v in pairs(profile:WaitForChild("RegionsFolder"):GetChildren()) do
table.insert(regions,v.Name)
end
table.insert(values,regions)
local sounds = {}
for i,v in pairs(profile:WaitForChild("SoundsFolder"):GetChildren()) do
table.insert(sounds,v.Name)
end
table.insert(values,sounds)
return values
else
return nil
end
end
function DS:GET(player)
local DataTable = nil
while true do
print("Fetching "..player.Name.."'s data.")
wait(5)
local status = pcall(function()
DataTable = DataStoreService:GetAsync(player.UserId)
end)
if status == true then
CREATE_PROFILE(player,DataTable)
break
else
print("Error fetching "..player.Name.."'s data. Retrying in 5s")
end
end
end
function DS:SET(player)
local values = GET_VALUES(player)
if values then
while true do
print("Saving "..player.Name.."'s data.")
local status = pcall(function()
DataStoreService:SetAsync(player.UserId,values)
end)
if status == true then
print("Successfully saved "..player.Name.."'s data.")
break
else
print("Error saving "..player.Name.."'s data. Retrying in 5s")
wait(5)
end
end
else
print("Player data not loaded, abandon save.")
end
end
return DS
Saving Roblox objects such as Folders and Values is much less efficient than saving the string of a JSON encoded table saved with the key of the UserId:
local DSS = game:GetService("DataStoreService")
local DataStore = DSS:GetDataStore("A2")
local HttpService = game:GetService("HttpService")
function createProfile(player)
local profile = {}
profile.playerId = player.UserId --UserId does not change, unlike username
profile.coins = 0
profile.currentSound = "Basic Breathe"
profile.UIScale = 1
profile.region = "Forest"
profile.breath = "Basic Breathe"
return profile
end
function loadProfile(player)
local profile
local success, err = pcall(function()
profile = DataStore:GetAsync(player.UserId)
end)
if not success then print(err) return {} end --Throws an error and ends the function if the :GetAsync fails
profile = HttpService:JSONDecode(profile)
return profile
end
function saveProfile(profile)
local playerId = profile.playerId
profile = HttpService:JSONEncode(profile)
local success, err = pcall(function()
DataStore:SetAsync(playerId, profile) -- Replace this with UpdateAsync when you understand datastores better, as it's a better method to use.
end)
if not success then print(err) return false
else return true end
end
Thanks for the suggestion. However, I have one question, the reason why I went with instances was due to the reason that clients could easily read data that they need, with tables, it seems much harder/slower to obtain information. Any suggestions?
I would suggest a Script or ModuleScript on the server which, when a player joins, adds their profile to a table and returns data on a RemoteEvent being invoked. So on the server your final script would look something like:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerService = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local DSS = game:GetService("DataStoreService")
local DataStore = DSS:GetDataStore("A2")
local playerProfiles = {}
function createProfile(player)
local profile = {}
profile.playerId = player.UserId --UserId does not change, unlike username
profile.coins = 0
profile.currentSound = "Basic Breathe"
profile.UIScale = 1
profile.region = "Forest"
profile.breath = "Basic Breathe"
return profile
end
function loadProfile(player)
local profile
local success, err = pcall(function()
profile = DataStore:GetAsync(player.UserId)
end)
if not success then print(err) return {} end --Throws an error and ends the function if the :GetAsync fails
profile = HttpService:JSONDecode(profile)
return profile
end
function saveProfile(profile)
local playerId = profile.playerId
profile = HttpService:JSONEncode(profile)
local success, err = pcall(function()
DataStore:SetAsync(playerId, profile) -- Replace this with UpdateAsync when you understand datastores better, as it's a better method to use.
end)
if not success then print(err) return false
else return true end
end
ReplicatedStorage.GetPlayerProfile.OnServerInvoke = function(player)
return playerProfiles[player.UserId]
end
-- When a player joins the game their data is loaded or generated, and they're added to the playerProfiles register
Players.PlayerAdded:Connect(function(player)
local profile = loadProfile(player)
if not profile then profile = createProfile(player)
playerProfiles[player.UserId] = profile
end)
-- When a player leaves the game their data is saved and removed from the active register
Players.PlayerRemoving:Connect(function(player)
local playerId = player.UserId
saveProfile(playerProfiles[playerId])
playerProfiles[playerId] = nil
end)
Then to get the player’s details, you can simply use a LocalScript to fire the RemoteFunction:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
function getProfile()
local profile = ReplicatedStorage.GetPlayerProfile:InvokeServer() -- No need to pass the player variable, as this is done automatically
return profile
end
-- Example for getting coins
function getCoins()
local profile = getData()
local coins = profile.coins
print("Player has "..coins.." coins.")
return coins
end
For a neater, organised touch, wrap actual methods with pcall rather than an anonymous function. Also, try making your function variables local rather than global.
local function loadProfile(player)
local success, result = pcall(DataStore.GetAsync, DataStore, player.UserId)
if not success then
warn(err)
return {}
end
return HttpService:JSONDecode(result)
end
local function saveProfile(profile)
local playerId = profile.playerId
local success, result = pcall(DataStore.SetAsync, DataStore, playerId, profile)
if not success then
warn(result)
end
return success -- Subsequent if statement is redundant
end
Whilst wrapping the actual methods could be a useful idea, I wrote them in anonymous functions to make it easier for 0lmx and others (including myself) to follow; I had the else statement there for the same reason. After all, “code is read much more than it is written”.
Sorry for replying so late, however, when I tried this method, it worked all fine, but then there was one specific issue… I have Guis in my project that display the inventory and coins of a player, which have to be constantly updated as they are ever changing, and I don’t think constantly firing remote events to update the data would be good…
For a GUI that is changing too often to be controlled by RemoteEvents, you would use two separate variables - one on the server, one on the client. Whenever a value is changed, it should be updated on the client side immediately, and updated on the server side as normal (that is, with checks to ensure no foul play is afoot).
Then, when displaying the value on a Gui you simply reference the client-side variable. There’s no risk here as if an exploiter changes this value it only changes the displayed value, rather than the server-side value used when purchasing items etc. in the example of coins.