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 ![]()
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)