--// DataHandler by actuallyarla! --// Settings local AccountAgeRequirement = 14 --// Number of days old that an account has to be to be able to play. Helps prevent exploiters and other bad actors. Set to 0 to disable. local DS = game:GetService("DataStoreService"):GetDataStore("PlayerData") --// datastore which saves all playerdata --[[ Hihi! Add to/edit the DataToStore table below to change the data that is stored for players! Entries under NormalData is data that is saved in ReplicatedStorage > PlayerData > (player name). This means that it can be seen by ANY player. Entries under SecureData is data that is saved in ServerStorage > SecureData > (player name), meaning it can only be seen by the server and the Player who the data belongs to. (by using the RemoteFunction in ReplicatedStorage.Events.Secure.ReturnSecureData) ValueType is the type of object that will be used in the explorer. This can be any of the following: - BoolValue (use this for storing true/false values) - BrickColorValue - CFrameValue - Color3Value - IntValue - NumberValue (use this for storing numbers) - ObjectValue - RayValue - StringValue (use this for storing text) - Vector3Value DefaultValue is the default value (obviously) ShouldSave determines whether or not you want to save that specific value to the DataStore after the player leaves. PutOnLeaderboard determines whether or not the value should be shown on the in-game leaderboard. Also, this is a bit more advanced, but if you would like to manually save a player's data, fire the BindableEvent at ServerStorage > Events > Data > SaveData with the player's Player object ]] --// CHANGE THIS TABLE TO CHANGE WHAT DATA GETS STORED local DataToStore = { NormalData = { NormalExample1 = { ValueType = "NumberValue", DefaultValue = 0, ShouldSave = true, PutOnLeaderboard = false, }, }, SecureData = { SecureExample1 = { ValueType = "NumberValue", DefaultValue = 0, ShouldSave = true, PutOnLeaderboard = false, }, } } --// You shouldn't need to change anything below this point, but who knows! --// Services local Players = game:GetService("Players") local Run = game:GetService("RunService") local ServerStorage = game:GetService("ServerStorage") local RepStorage = game:GetService("ReplicatedStorage") --// Setup if not workspace:FindFirstChild("Players") then local PlayerFolder = Instance.new("Folder") PlayerFolder.Name = "Players" PlayerFolder.Parent = workspace end if not RepStorage:FindFirstChild("PlayerData") then local fol = Instance.new("Folder") fol.Name = "PlayerData" fol.Parent = RepStorage end if not ServerStorage:FindFirstChild("SecureData") then local fol = Instance.new("Folder") fol.Name = "SecureData" fol.Parent = ServerStorage end if not RepStorage:FindFirstChild("Events") then local fol = Instance.new("Folder") fol.Name = "Events" fol.Parent = RepStorage end if not RepStorage.Events:FindFirstChild("Secure") then local fol = Instance.new("Folder") fol.Name = "Secure" fol.Parent = RepStorage.Events end if not RepStorage.Events.Secure:FindFirstChild("ReturnSecureData") then local event = Instance.new("RemoteFunction") event.Name = "ReturnSecureData" event.Parent = RepStorage.Events.Secure end if not ServerStorage:FindFirstChild("Events") then local fol = Instance.new("Folder") fol.Name = "Events" fol.Parent = ServerStorage end if not ServerStorage.Events:FindFirstChild("Data") then local fol = Instance.new("Folder") fol.Name = "Data" fol.Parent = ServerStorage.Events end if not ServerStorage.Events.Data:FindFirstChild("SaveData") then local event = Instance.new("BindableEvent") event.Name = "SaveData" event.Parent = ServerStorage.Events.Data end --// Functions local function SaveToDS(Player) local DataFolder = RepStorage.PlayerData:FindFirstChild(Player.Name) local SecureDataFolder = ServerStorage.SecureData:FindFirstChild(Player.Name) if not DataFolder or not SecureDataFolder then error("data folder or secure data folder couldnt be found for "..Player.Name) end local NewPlayerData = {} --// adds regular data to the "NewPlayerData" table created above for key,value in pairs(DataToStore["NormalData"]) do if not value["ShouldSave"] then continue end --// if value isn't meant to save to datastores, don't save it! if DataFolder:FindFirstChild(key) then NewPlayerData[key] = DataFolder:FindFirstChild(key).Value else NewPlayerData[key] = value["DefaultValue"] end end --// adds secure data to the "NewPlayerData" table for saving for key,value in pairs(DataToStore["SecureData"]) do if not value["ShouldSave"] then continue end if SecureDataFolder:FindFirstChild(key) then NewPlayerData[key] = SecureDataFolder:FindFirstChild(key).Value else warn("didn't find "..key) NewPlayerData[key] = value["DefaultValue"] end end local Attempts = 0 local UserId = Player.UserId local Name = Player.Name local function SaveData() local s,r --// this makes sure that the player's data is actually loaded before saving it! if RepStorage.PlayerData:FindFirstChild(Player.Name) and RepStorage.PlayerData[Player.Name]:FindFirstChild("DataLoaded") ~= nil and RepStorage.PlayerData[Player.Name].DataLoaded.Value == true then s,r = pcall(function() return DS:SetAsync(UserId,NewPlayerData) end) else s,r = false,nil end if s and r then return r end if Attempts <= 5 then task.wait((Attempts + 1)^2) --// waits a longer amount each time based on how many previous attempts as to not overload our datastore limit, anywhere from 4 to 36 seconds --// if this reaches 5 attempts, that's a total of 90 seconds. if it fails the 5th time, it gives up Attempts += 1 SaveData() else error("Data saving FAILED for "..Name) end end SaveData() end --// Real code Players.PlayerAdded:Connect(function(Player) --// account age requirement, with exemption for testing if Player.AccountAge < AccountAgeRequirement and not Run:IsStudio() then Player:Kick("Your account must be at least "..AccountAgeRequirement.."days old to play this game in order to prevent exploiting. You have "..AccountAgeRequirement - Player.AccountAge.."days remaining until you can play. Sorry!") return end --// creates the folders to enclose player values, one for normal data that all clients can be trusted with, and one for secure data like admin status and other stuff we don't want exploiters seeing local DataFolder = Instance.new("Folder") DataFolder.Name = Player.Name DataFolder.Parent = RepStorage.PlayerData local SecureDataFolder = Instance.new("Folder") SecureDataFolder.Name = Player.Name SecureDataFolder.Parent = ServerStorage.SecureData --// creates boolvalue which will update when the data has loaded from the datastore local DataLoaded = Instance.new("BoolValue") DataLoaded.Value = false --// sets by default to false so this isn't really needed, but better safe than sorry! DataLoaded.Name = "DataLoaded" DataLoaded.Parent = DataFolder --// creates values that the player can access for key,value in pairs(DataToStore["NormalData"]) do local NewValue = Instance.new(value["ValueType"]) NewValue.Name = key if value["DefaultValue"] then --// makes it so we can choose to not set a value, like in the case of folders NewValue.Value = value["DefaultValue"] end NewValue.Parent = DataFolder if value["PutOnLeaderboard"] then if not Player:FindFirstChild("leaderstats") then local leaderstats = Instance.new("Folder") leaderstats.Name = "leaderstats" leaderstats.Parent = Player end local LeaderstatsValue = Instance.new(value["ValueType"]) LeaderstatsValue.Name = key LeaderstatsValue.Parent = Player.leaderstats NewValue.Changed:Connect(function() LeaderstatsValue.Value = NewValue.Value end) end end --// creates super secret secure values for key,value in pairs(DataToStore["SecureData"]) do local NewValue = Instance.new(value["ValueType"]) NewValue.Name = key NewValue.Value = value["DefaultValue"] NewValue.Parent = SecureDataFolder if value["PutOnLeaderboard"] then if not Player:FindFirstChild("leaderstats") then local leaderstats = Instance.new("Folder") leaderstats.Name = "leaderstats" leaderstats.Parent = Player end local LeaderstatsValue = Instance.new(value["ValueType"]) LeaderstatsValue.Name = key LeaderstatsValue.Parent = Player.leaderstats NewValue.Changed:Connect(function() LeaderstatsValue.Value = NewValue.Value end) end end local Success,Data = pcall(function() return DS:GetAsync(Player.UserId) end) if Success and Data then --// player is returning, and has data! for key,value in pairs(Data) do if DataToStore["NormalData"][key] and DataFolder[key] then --// data should be loaded to the normal data folder DataFolder[key].Value = value elseif DataToStore["SecureData"][key] and SecureDataFolder[key] then --// data should be loaded to secure data folder SecureDataFolder[key].Value = value else warn("something went wrong with "..Player.Name.."'s data, "..key.." couldn't be found in any folder, value was "..value) end end DataLoaded.Value = true elseif Success and not Data then --// player is new, leave values as default! DataLoaded.Value = true elseif not Success then --// datastore request failed :( warn("Data failed to load for "..Player.Name..". Error: "..Data) DataFolder:Destroy() SecureDataFolder:Destroy() Player:Kick("Your data failed to load, please try rejoining!") end end) --// saves player's data, won't work properly in studio because the server shuts down too soon, but will work in main game (i think) Players.PlayerRemoving:Connect(function(Player) SaveToDS(Player) local DataFolder = RepStorage.PlayerData:FindFirstChild(Player.Name) local SecureDataFolder = ServerStorage.SecureData:FindFirstChild(Player.Name) if DataFolder then DataFolder:Destroy() end if SecureDataFolder then SecureDataFolder:Destroy() end end) RepStorage.Events.Secure.ReturnSecureData.OnServerInvoke = function(Player) --// send own secure data to player that requested it (can't be exploited) local DataList = {} for _,v in ipairs(ServerStorage.SecureData:WaitForChild(Player.Name):GetChildren()) do DataList[v.Name] = v.Value end return DataList end ServerStorage.Events.Data.SaveData.Event:Connect(function(Player) --// if a player (most likely dev) wants to manually save data if type(Player) == "string" then if Players:FindFirstChild(Player) then SaveToDS(Players[Player]) end elseif Player:IsA("Player") then SaveToDS(Player) end end)