DataScript advice

Hey guys, I’m currently working on the data system for my simulator/incremental game.

In my previous projects, I used NumberValues and StringValues, but this time I wanted to try a different approach that’s supposed to be more efficient and scalable

With the help of AI, I ended up building this new Data Script, and I’d like some feedback on it. I’m not very experienced with scripting yet, so I’m not sure if it’s solid, if it could cause bugs, or if it might have security issues or be exploitable.

Any feedback or advice would be appreciated :+1:


local DataStoreService = game:GetService("DataStoreService")
local playerDataStore = DataStoreService:GetDataStore("PlayerData")
local EN = require(game.ReplicatedStorage.EternityNum)

-- PLAYER DATA TABLES
local DataManager = require(game.ServerScriptService.PlayerDataManager)
local PlayerData = DataManager.PlayerData
local SessionData = DataManager.SessionData

-- TEMPLATE (auto used for new players)
local TEMPLATE = {
	Stats = {
		Aura = 0,
		Chapter = 1,
		Fragments = "0",
		Essence = "0",
		Radiance = "0",
		Chi = "0",
	},

	Profile = {
		JoinDate = nil,
		TimePlayed = 0,
		DaysPlayed = 0,
		DaysStreak = 0,
		RobuxSpent = 0,
		RunesOpened = 0,
		RuneLuck = 0,
		RuneBulk = 0,
	}
}

-- UTILS
local function deepCopy(tbl)
	local copy = {}
	for k,v in pairs(tbl) do
		copy[k] = typeof(v) == "table" and deepCopy(v) or v
	end
	return copy
end

local function shortEN(str)
	return EN.short(EN.fromString(str), 2)
end

-- SAVE FUNCTION
local function savePlayer(player)
	local data = PlayerData[player]
	if not data then return end

	local success, err = pcall(function()
		playerDataStore:SetAsync(player.UserId, data)
	end)

	if not success then
		warn("SAVE FAILED:", err)
	end
end

local function reconcile(template, data)
	for key, value in pairs(template) do
		if data[key] == nil then
			data[key] = typeof(value) == "table" and deepCopy(value) or value -- missing value -> add it
		elseif typeof(value) == "table" then
			reconcile(value, data[key]) -- if it's a table, go deeper (recursive merge)
		end
	end
end

-- PLAYER JOIN
game.Players.PlayerAdded:Connect(function(player)
	-- LOAD DATA
	local data
	local success = pcall(function()
		data = playerDataStore:GetAsync(player.UserId)
	end)

	if not data then data = deepCopy(TEMPLATE)
	else reconcile(TEMPLATE, data) end

	PlayerData[player] = data

	-- session data
	SessionData[player] = {
		JoinTime = os.time()
	}

	-- JOIN DATE / DAILY LOGIN
	local today = os.date("%Y-%m-%d")

	if not data.Profile.JoinDate then
		data.Profile.JoinDate = today
	end

	-- UI FOLDERS (DISPLAY ONLY)
	local leaderstats = Instance.new("Folder", player)
	leaderstats.Name = "leaderstats"

	local auraDisplay = Instance.new("StringValue", leaderstats)
	auraDisplay.Name = "Aura"

	local fragmentsDisplay = Instance.new("StringValue", leaderstats)
	fragmentsDisplay.Name = "‎Fragments"

	local essenceDisplay = Instance.new("StringValue", leaderstats)
	essenceDisplay.Name = "Essence"

	-- LEADERSTATS LOOP
	task.spawn(function()
		while player.Parent do
			local stats = PlayerData[player].Stats

			fragmentsDisplay.Value = shortEN(stats.Fragments)
			essenceDisplay.Value = shortEN(stats.Essence)

			task.wait(0.25)
		end
	end)

	-- TIME PLAYED LOOP
	task.spawn(function()
		while player.Parent do
			task.wait(60)
			PlayerData[player].Profile.TimePlayed += 1
		end
	end)

	-- AUTOSAVE LOOP
	task.spawn(function()
		while player.Parent do
			task.wait(120)
			savePlayer(player)
		end
	end)

end)

-- PLAYER LEAVE
game.Players.PlayerRemoving:Connect(function(player)
	if SessionData[player] then
		local sessionLength = os.time() - SessionData[player].JoinTime
		PlayerData[player].Profile.TimePlayed += math.floor(sessionLength / 60)
	end

	savePlayer(player)

	PlayerData[player] = nil
	SessionData[player] = nil
end)

-- SERVER CLOSE
game:BindToClose(function()
	for _, player in pairs(game.Players:GetPlayers()) do
		savePlayer(player)
	end
	task.wait(3)
end)

3 Likes

It looks good but you shouldn’t fully rely game:BindToClose at some cases

because sometimes theres different reasons game to be shutdown

game:BindToClose(function(Reason)    -- My bind to close function script
	local WaitTime = 0
	BindToClose = true
	
	if Reason == Enum.CloseReason.OutOfMemory or Reason == Enum.CloseReason.RobloxMaintenance or Reason == Enum.CloseReason.Unknown then
		error("Emergency shutdown reason: "..Reason)
		return false
	end
	
	for int, LocalPlayer in ipairs(Players:GetPlayers()) do
		WaitTime += 3
		
		if SavedOnExit[LocalPlayer.UserId] then
			continue
		end
		
		task.spawn(function()	
			SaveSlot(LocalPlayer, true)
		end)
	end
	
	task.wait(WaitTime)
end)

Sometimes server shutdowns so instant that script doesn’t get enough of time to save (I didn’t tested this on less process my script needs 2-4 seconds per save)

So its much better to rely on autosave incase of server getting shutdown by reason (OutOfMemory, Unknown, RobloxMaintenance) Idk if RobloxMaintenance does give time to shutdown

without that i didn’t see any problem looks good for me

1 Like

Pretty alright for someone who is still learning! The reconcile function is a nice touch, some beginners skip this part and then wonder why returning players get errors when new fields are added.

But there are a few things to look at:

  • The BindToClose wait of 3 seconds might not be enough if you have a full server. Each SetAsync call can take up to a few seconds, just like how @denizpimik has said, and they’re running sequentially here. So consider using UpdateAsync instead of SetAsync as a general habit too, it’s safer for avoiding data overwrites.
  • The leaderstats update loop running every 0.25s is pretty frequent. Not a huge deal for small servers but it’ll add up. 1-2 seconds is usually fine for display values.
  • Your pcall isn’t catching the result correctly, you’re only storing the success boolean but ignoring the second return value (the error).

But overall the structure is clean, good job!!

1 Like