Broken Players Data Management

Hello everyone.

Sorry, but this will be a very long message, I hope you have the patience to read it all.

I recently, unfortunately, released my game on Roblox, and it was a disaster. I don’t know how it’s possible, but the data of new players is being initialized with data from players already present in the server, and I really can’t explain how this is happening…

I have a module to manage the players data. this is my load function:

local function PlayerAdded(player)

	--database:SetAsync(player.UserId,defaultData)
	local success, playerData = nil, nil
	local attempt = 1

	repeat
		success, playerData = pcall(function()
			return database:GetAsync(player.UserId)
		end)
		attempt += 1
		if not success then
			warn(playerData)
			task.wait(3)
		end
	until success or attempt == 5

	if success then
		print("Connected to the database")
		if not playerData then
			print("Assigning default data")
			playerData = defaultData
			playerData.name = player.Name
		else
			-- Aggiorna i dati del giocatore con i valori predefiniti mancanti
			for key, value in pairs(defaultData) do
				if playerData[key] == nil then
					playerData[key] = value
					print("il valore " .. tostring(key) .. " era assente nel salvataggio, viene quindi aggiunto con valore default " .. tostring(defaultData[key]))
				end
			end
		end
		playerData.startTick = tick()
		sessionDataBase[player.UserId] = playerData
		sessionDataBase[player.UserId].frostWalk = 0
		setLeaderStats(player)
		unlockAchievement(player, "firstAccessAchievement")
		if player.MembershipType == Enum.MembershipType.Premium then
			unlockAchievement(player, "premiumFirstAccessAchievement")
		end
		PlayerDataManager.setFlyAbility(player)
		print("Data loaded")
	else
		warn("Unable to get data for player", player.UserId)
		player:Kick("Unable to load your data, try again later")
	end
end
PlayerService.PlayerAdded:Connect(PlayerAdded)

Save function:

local function save(player, quitting)
	
	print("Saving...")
	if sessionDataBase[player.UserId] then
		
		updatePlayTime(player)
		local success, errorMsg = nil, nil
		local attempt = 1

		repeat
			success, errorMsg = pcall(function()
				database:SetAsync(player.UserId, sessionDataBase[player.UserId])
			end)
			attempt += 1
			if not success then
				warn(errorMsg)
				task.wait(1)
			end
		until success or attempt == 5
		if success then
			print("Saved")
		end
	end
	if not quitting then
		task.spawn(checkPlayTime, player)
		--print(tableToString(sessionDataBase[player.UserId]))
	end
	
end

local saving = false
local function tryToSave(player)
	if saving then
		print("Already saving, retrying in a bit...")
		task.spawn(function()
			task.wait(1)
			tryToSave(player) -- riprova in background
		end)
		return
	end

	local timeFromLastSave = tick() - lastSave
	if timeFromLastSave < 20 then
		print("Not Saved because time from last save is " .. timeFromLastSave .. " seconds")
		return
	else 
		saving = true
		save(player)
		lastSave = tick()
		saving = false
	end
end

default data:


local defaultData = {

	["maxHp"] = 10,
	["hp"] = 10,
	["hpRegen"] = 0,
	["walkSpeed"] = 20,
	["speed"] = 25,
	["jumpForce"] = 7,
	["maxJumps"] = 1,
	["fly"] = 0,
	["teleport"] = 0,
	
	["currentStage"] = 0,
	["totalStage"] = 0,
	
	["rebirth"] = 0,
	["rebirthPoints"] = 0,
	["totalRebirthPoints"] = 0,
	["rebirthPointsBonus"] = 0,
	["rebirthPointsMultiplier"] = 1,
	
	["currentDeaths"] = 0,
	["totalDeaths"] = 0,
	
	["startTick"] = 0,
	["totalPlayTime"] = 0,
	
	["robuxDonated"] = 0,
	["robuxSpent"] = 0,
	
	["secretsFound"] = 0,
	["unlockedAchievement"] = {},
	
	["rank"] = "Noob",
	["ranks"] = {},
	["settings"] = {true, true, true, true, true, true, true, true, true},
	
	["codes"] = {},
	
	["levelProgression"] = true,
	
	["unlockedSkills"] = {},
	
	["badgeAwarded"] = {},
	
	["frostWalk"] = 0
}

As you can see, what I do is use a session table for the players in the server, which is periodically and at the end of the session saved to the database. If the data doesn’t exist, I should be using the default ones, but as mentioned, this isn’t happening. I’m desperate because I’ve been developing this game alone for 3 months, trying to make it perfect, and now I see the main and most important component isn’t working. I should add that the server might be overloaded with work because I have many tweens running server-side. I’m not sure if this could affect it.

Well, without reading much into it, you’re setting playerData directly to defaultData without cloning it first, meaning changing playerData will directly affect defaultData as well.

You can use this piece of code to deep clone the defaultData:

local function deepClone<T>(t: T): T
	assert(typeof(t) == "table")
	t = table.clone(t) :: any

	for index: any, value: any in t do
		if typeof(value) == "table" then
			t[index] = deepClone(value)
		end
	end

	return t
end

-- your code
playerData = deepClone(defaultData)
3 Likes

Thank you so much! It really shows my inexperience :frowning: I had no idea that assigning a table meant assigning the memory address. As soon as I get a chance to test it, if it works, I’ll mark your response as the solution <3

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.