Players experience data loss, storing data sometimes fails so I put it in a loop until it saves (I don’t know if it’s the correct way) Tried to recover old data → to the new datastore. The code is very messy because of time stress… but I can’t figure out the errors. It works for me but others it keep loading their old data probably from the “Recovery” module that recovers the old data and puts it in the new data.
Player Data (Server Script)
--[[
Date : 2025-04-14
]]
-- Services
local DataStoreService = game:GetService("DataStoreService")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
-- Modules
local SessionManager = require(script.SessionManager)
local Commands = require(script.Commands)
local Recovery = require(script.Recovery)
-- All player data is stored here...
local UserData = DataStoreService:GetDataStore("User Data")
-- Table of user data
local UserTable = {}
local UserConnections = {}
-- Retrieve data from the specific user using their user id
local function RetrieveData(userId: number, ignoreLockedSessions: boolean?): string
if ignoreLockedSessions == nil then
ignoreLockedSessions = false
end
if ignoreLockedSessions == true then
SessionManager.Claim(userId)
end
local success: boolean, result: string = pcall(function()
return UserData:GetAsync(userId)
end)
if success then
print(string.format("Successfully retrieved data from user: %s 🟩", tostring(userId)))
return result
end
warn(string.format("Failed to retrieve data from user: %s 🟥", tostring(userId)))
return "Error"
end
-- Stores data to the specific user using their user id
local function StoreData(userId: number, data): boolean
if not SessionManager.IsOwner(userId) then
warn(string.format("Skipping save: session lock expired or stolen for user {%s}", tostring(userId)))
return false
end
local success: boolean, result: string = pcall(function()
UserData:SetAsync(userId, data)
end)
if success then
print(string.format("Successfully stored data to user: %s 🟩", tostring(userId)))
return true
end
warn(string.format("Failed to store data to user: %s 🟥", tostring(userId)))
return false
end
-- When client is joining the experience
local function OnJoin(client: Player)
-- User data
--[[
Points
Medals
]]
local connections: { RBXScriptConnection } = {}
local data = {
["Points"] = 0,
["Medals"] = {
["Gold"] = 0,
["Silver"] = 0,
["Bronze"] = 0
}
}
-- Function to retrieve user data from the server!
local retrievedData: string = RetrieveData(client.UserId)
-- Checks if you get an error...
-- If an error occurs it will repeat every 60 seconds until it isn't an error!
while true do
if retrievedData ~= "Error" then
if retrievedData == nil then
-- Recovers old data if its possible
local oldData = Recovery:Retrieve(client.UserId)
if oldData.Points ~= nil then
data["Points"] = oldData.Points
end
if oldData.Medals.Gold ~= nil and oldData.Medals.Silver ~= nil and oldData.Medals.Bronze ~= nil then
data["Medals"]["Gold"] = oldData.Medals.Gold
data["Medals"]["Silver"] = oldData.Medals.Silver
data["Medals"]["Bronze"] = oldData.Medals.Bronze
end
break
end
data = HttpService:JSONDecode(retrievedData)
break
end
task.wait(60)
retrievedData = RetrieveData(client.UserId)
end
UserTable[tostring(client.UserId)] = data
print(UserTable)
-- Leaderboard
local leaderstats: Configuration = Instance.new("Configuration")
leaderstats.Name = "leaderstats"
local medals: Configuration = Instance.new("Configuration")
medals.Name = "Medals"
-- Stats inside of the leaderstats
local statistics: { Rank: StringValue, Points: IntValue } = {
Rank = Instance.new("StringValue"),
Points = Instance.new("IntValue")
}
-- Hidden stats inside of the hidden stats
local medalsStats: { Gold: IntValue, Silver: IntValue, Bronze: IntValue } = {
Gold = Instance.new("IntValue"),
Silver = Instance.new("IntValue"),
Bronze = Instance.new("IntValue")
}
do -- Initializing statistics
do -- Rank
statistics.Rank.Name = "Rank"
statistics.Rank.Value = client:GetRoleInGroup(9344569) or "Guest"
statistics.Rank.Parent = leaderstats
end
do -- Points
statistics.Points.Name = "Point(s)"
statistics.Points.Value = UserTable[tostring(client.UserId)]["Points"]
table.insert(connections, statistics.Points:GetPropertyChangedSignal("Value"):Connect(function()
UserTable[tostring(client.UserId)]["Points"] = statistics.Points.Value
end))
statistics.Points.Parent = leaderstats
end
end
do -- Initializing hidden statistics
do -- Gold
medalsStats.Gold.Name = "Gold"
medalsStats.Gold.Value = UserTable[tostring(client.UserId)]["Medals"]["Gold"]
table.insert(connections, medalsStats.Gold:GetPropertyChangedSignal("Value"):Connect(function()
UserTable[tostring(client.UserId)]["Medals"]["Gold"] = medalsStats.Gold.Value
end))
medalsStats.Gold.Parent = medals
end
do -- Silver
medalsStats.Silver.Name = "Silver"
medalsStats.Silver.Value = UserTable[tostring(client.UserId)]["Medals"]["Silver"]
table.insert(connections, medalsStats.Silver:GetPropertyChangedSignal("Value"):Connect(function()
UserTable[tostring(client.UserId)]["Medals"]["Silver"] = medalsStats.Silver.Value
end))
medalsStats.Silver.Parent = medals
end
do -- Bronze
medalsStats.Bronze.Name = "Bronze"
medalsStats.Bronze.Value = UserTable[tostring(client.UserId)]["Medals"]["Bronze"]
table.insert(connections, medalsStats.Bronze:GetPropertyChangedSignal("Value"):Connect(function()
UserTable[tostring(client.UserId)]["Medals"]["Bronze"] = medalsStats.Bronze.Value
end))
medalsStats.Bronze.Parent = medals
end
end
-- Set leaderstats and data parent to the client so it will be fully loaded when it's attached to the client!
leaderstats.Parent = client
medals.Parent = client
UserConnections[client.UserId] = connections
repeat
task.wait(30)
statistics.Points.Value += 1
until client == nil
end
-- When client is leaving the game
local function OnLeave(client: Player)
local userId: number = client.UserId
-- Disables all user connections to specific user!
if UserConnections[userId] ~= nil then
for index: number, value: RBXScriptConnection in UserConnections[userId] do
value:Disconnect()
end
UserConnections[userId] = nil
end
local userTable = UserTable[tostring(userId)]
if not userTable then
warn(string.format("Can't store data to user: %s\nBecause of no retrieved data! 🟥⚠️", tostring(userId)))
return
end
while true do
local result: boolean = StoreData(userId, HttpService:JSONEncode(userTable))
if result == true then
UserTable[tostring(userId)] = nil
print(UserTable)
break
end
task.wait(60)
end
SessionManager.Release(userId)
end
-- Connecting events to functions
Players.PlayerAdded:Connect(OnJoin)
Players.PlayerRemoving:Connect(OnLeave)
-- Whenever the game crashes or closes
game:BindToClose(function()
if RunService:IsStudio() then warn("Ignoring crash storing while in studio! ⚠️") return end
for index: number, client: Player in Players:GetPlayers() do
OnLeave(client)
end
end)
script.Retrieve.OnInvoke = RetrieveData
script.Store.OnInvoke = StoreData
Commands.Initialize()
Recovery (Module Script)
local DataStoreService = game:GetService("DataStoreService")
local points = DataStoreService:GetDataStore("myDataStore")
local medals = DataStoreService:GetDataStore("MedalsData")
local Recovery = {}
-- Retrieves the old data from the user and makes it into a table instead
function Recovery:Retrieve(userId: number): { Points: number, Medals: { Gold: number, Silver: number, Bronze: number }}
local data = {
["Points"] = nil,
["Medals"] = {
["Gold"] = nil,
["Silver"] = nil,
["Bronze"] = nil
}
}
do -- Points
local function RetrievePoints()
local success: boolean, result: string = pcall(function()
return points:GetAsync("Player" .. userId)
end)
if success and result ~= nil then
print(string.format("Successfully recovered old point data from user: %s 🟩", tostring(userId)))
data["Points"] = result
return
elseif not success then
return "Error"
end
end
-- Checks if you get an error...
-- If an error occurs it will repeat every 60 seconds until it isn't an error!
while true do
local message = RetrievePoints()
if message ~= "Error" then
break
end
task.wait(60)
end
end
do -- Medals
local function RetrieveMedals()
local success: boolean, result: string = pcall(function()
return medals:GetAsync(userId)
end)
if success and result ~= nil then
print(string.format("Successfully recovered old medal data from user: %s 🟩", tostring(userId)))
local medalValues = string.split(result, ':')
if #medalValues == 3 then
data["Medals"]["Gold"] = medalValues[1]
data["Medals"]["Silver"] = medalValues[2]
data["Medals"]["Bronze"] = medalValues[3]
return
end
end
if not success then
return "Error"
end
end
while true do
local message = RetrieveMedals()
if message ~= "Error" then
break
end
task.wait(60)
end
end
return data
end
return Recovery
SessionManager (Module Script)
local MemoryStoreService = game:GetService("MemoryStoreService")
local SessionLocks = MemoryStoreService:GetSortedMap("SessionLocks")
local SessionManager = {}
-- Duration before session is considered expired (in seconds)
local SESSION_TIMEOUT = 60*5
-- Claim a session for a user and wait for any existing one to expire
function SessionManager.Claim(userId: number): boolean
local sessionKey: string = tostring(userId)
local now: number = os.time()
local attempts: number = 10
while attempts > 0 do
local success, timestamp = pcall(function()
return SessionLocks:GetAsync(sessionKey)
end)
if success and timestamp ~= nil then
local age = now - timestamp
if age < SESSION_TIMEOUT then
task.wait(5)
attempts -= 1
else
break -- expired session, we can proceed
end
else
break -- no session or failure, proceed
end
end
local success, err = pcall(function()
SessionLocks:SetAsync(sessionKey, now, SESSION_TIMEOUT)
end)
if success then
return true
else
warn(`[SessionManager] Failed to claim session for {userId}: {err}`)
return false
end
end
-- Check if we still hold the session before saving
function SessionManager.IsOwner(userId: number): boolean
local sessionKey: string = tostring(userId)
local now: number = os.time()
local success, timestamp = pcall(function()
return SessionLocks:GetAsync(sessionKey)
end)
if success and timestamp ~= nil then
local age = now - timestamp
return age <= SESSION_TIMEOUT
end
return false
end
-- Release the session after saving
function SessionManager.Release(userId: number)
local sessionKey = tostring(userId)
pcall(function()
SessionLocks:RemoveAsync(sessionKey)
end)
end
return SessionManager