Hi All,
I’ll keep this brief. I’m looking for feedback on my saving system. I’ve omitted many parts of the code to keep this post succinct, but I’m looking for specific feedback on how the overall architecture can be improved to mitigate the risk of data loss. Any feedback at all would be much appreciated! I’ve also commented the script to make the intention between certain parts clear.
--[[ SERVICES ]]--
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local RunService = game:GetService("RunService")
--[[ VARIABLES ]]--
-- Datastore
local DataStore = DataStoreService:GetDataStore("MyDatabase")
-- Players that have been loaded in
local loadedPlayers = {} -- This table is used to constrain saving for only fully loaded players
local savingPlayers = {} -- This table is to check whether a player is already being saved
-- Overall player save variables
local plrSaves = {} -- Stores result from GetAsync()
--[[ FUNCTIONS ]]--
--[[ Saving Functions ]]--
-- General Save
local function Save(plr)
-- Checks if the player is loaded before saving
if not loadedPlayers[plr.UserId] or not plrSaves[plr.UserId] then return end
-- Checks if the player data is already being saved
if savingPlayers[plr.UserId] then return end
-- Indicates that the player is currently saving // constrains further calls
savingPlayers[plr.UserId] = true
--[[
OMITTED SECTION 1:
* This is where I save each of the player statistics, inventory, and placed items
* If anything returns nil, or false, I exit without saving to prevent data being overwritten
]]--
-- Pseudocode, but basic logic of the current system
if anythingFromAboveReturnedNil or anythingFromAboveFailed then return end
local key = "plr-"..plr.UserId
local success, err = pcall(function()
DataStore:UpdateAsync(key, function(oldData)
-- Creates default data if the player is new
if not oldData then
oldData = {["Current"] = nil, ["Save1"] = {}, ["Save2"] = {}, ["Save3"] = {}}
end
local newData = oldData
--[[
OMITTED SECTION 2:
* This is where I save all the tables, variables, etc. generated from OMITTED SECTION 1
* I save everything into the table newData and return this table
]]--
-- Updates local copy of saves
plrSaves[plr.UserId] = newData
return newData
end)
end)
if not success then warn("Failed to overwrite data: " ..tostring(err)) end
-- Resets saving bit to allow further calls
savingPlayers[plr.UserId] = nil
end
-- Saves all players // simply calls above function
local function SaveLoadedPlayers()
for _, player in pairs(Players:GetPlayers()) do
Save(player)
end
end
--[[ Loading Functions ]]--
-- Loading function
local function Load(plr)
local key = "plr-"..plr.UserId
local succ, err = false, ""
local savedData = nil
local count = 0
-- Repeat fire for GetAsync() // 5 repeated calls in case of failure
repeat
succ, err = pcall(function()
savedData = DataStore:GetAsync(key)
end)
if not savedData then wait(6) end
count = count + 1
until succ or count == 5
-- Failed to read data
if not succ then
warn("Failed to read data: " ..tostring(err))
return
end
plrSaves[plr.UserId] = savedData or {["Current"] = nil, ["Save1"] = {}, ["Save2"] = {}, ["Save3"] = {}}
return savedData
end
--[[ EVENT CONNECTIONS ]]--
-- Loading player data on player join
Players.PlayerAdded:Connect(Load)
-- Saving player data and resetting tycoon on player leave
Players.PlayerRemoving:Connect(function(plr)
-- Checks if the player is already being saved
if not savingPlayers[plr.UserId] then
Save(plr)
else
repeat wait(2) until not savingPlayers[plr.UserId] -- waits until save is complete
end
--[[
OMITTED SECTION 3
* Here I delete everything the player has placed since the player should be
guaranteed to have been saved from above block.
* Deleting items while the player saves can lead to corruption, hence the
repeat wait(2) used above.
]]--
-- Clearing memory from tables
plrSaves[plr.UserId] = nil
loadedPlayers[plr.UserId] = nil
end)
-- Saving all player data on server shutdown ~ no need to clear memory
game:BindToClose(function()
-- Multithreaded saving
for _, player in pairs(Players:GetPlayers()) do
-- Already saving player // no need to resave
if savingPlayers[player.UserId] then continue end
-- Saving new player
coroutine.wrap(function()
Save(player)
end)()
end
if RunService:IsStudio() then
wait(5) -- small enough wait to save my own data when running on studio
else
wait(30) -- waits maximum allowed time
end
end)
--[[ INIT ]]--
-- Autosave player data
do
local autosaveTime = 480
while wait(autosaveTime) do
SaveLoadedPlayers()
end
end