Tower Defense game having tons of Data errors

Hello developers, I am making a tower defense game but there seems to be data errors everywhere. I dont know what is causing this and dont know how to fix it. There are data errors for the leaderstats XP/coins and there are data errors for selected towers in the GAME MATCH which is the teleported server where the actual tower defense game starts. Here is the first part of the data errors starting in the lobby game:


And here is the script for the ShopServer which buys towers from a gui:

ShopServer script in lobby server:

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local database = DataStoreService:GetDataStore("database7")
local towers = require(ReplicatedStorage:WaitForChild("TowerShop"))

local MAX_SELECTED_TOWERS = 5
local updateCurrencyEvent = ReplicatedStorage:WaitForChild("UpdateCurrency")

local data = {}

-- Load the players data
local function LoadData(player)
	local success = nil
	local playerData = 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()
		end
		
	until success or attempt == 3
	
	if success then
		print("Connection success")
		if not playerData then
			print("New player, giving default data")
			playerData = {
				["Stars"] = 100,
				["SelectedTowers"] = {"Pz. II"},
				["OwnedTowers"] = {"Pz. II", "M2 Light"}
			}
		end
		data[player.UserId] = playerData
		updateCurrencyEvent:FireClient(player, playerData.Stars) 
	else
		warn("Unable to get data for player", player.UserId)
		player:Kick("There was a problem getting your data")
	end
end
Players.PlayerAdded:Connect(LoadData)

-- Save the players data
local function SaveData(player)
	if data[player.UserId] then
		local success = nil
		local playerData = nil
		local attempt = 1

		repeat
			success, playerData = pcall(function()
				return database:UpdateAsync(player.UserId, function()
					return data[player.UserId]
				end)
			end)

			attempt += 1
			if not success then
				warn(playerData)
				task.wait()
			end

		until success or attempt == 3

		if success then
			print("Data saved successfully")
		else
			warn("Unable to save data for", player.UserId)
		end
	else
		warn("No session data for", player.UserId)
	end
	
end
Players.PlayerRemoving:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

game:BindToClose(function()
	if not RunService:IsStudio() then
		for index, player in pairs(Players:GetPlayers()) do
			task.spawn(function()
				SaveData(player)
			end)
		end
	else
		print("Shutting down inside studio")
	end
end)

local function getItemStatus(player, itemName)
	local playerData = data[player.UserId]
	if table.find(playerData.SelectedTowers, itemName) then
		return "Equipped"
	elseif table.find(playerData.OwnedTowers, itemName) then
		return "Owned"
	else
		return "For Sale"
	end
end

local function updatePlayerLoadout(player)
	local playerData = data[player.UserId]
	if playerData then
		local loadout = {}
		for i = 1, MAX_SELECTED_TOWERS do
			loadout[i] = playerData.SelectedTowers[i] or ""
		end
		ReplicatedStorage.UpdateLoadout:FireClient(player, loadout)
	end
end

local function updatePlayerData(player, itemName, action)
	local playerData = data[player.UserId]
	if action == "For Sale" then
		playerData.Stars -= towers[itemName].Price
		table.insert(playerData.OwnedTowers, itemName)
	elseif action == "Owned" then
		table.insert(playerData.SelectedTowers, itemName)
		if #playerData.SelectedTowers > MAX_SELECTED_TOWERS then
			table.remove(playerData.SelectedTowers, 1)
		end
	elseif action == "Equipped" then
		if #playerData.SelectedTowers > 1 then
			local towerToRemove = table.find(playerData.SelectedTowers, itemName)
			table.remove(playerData.SelectedTowers, towerToRemove)
		end
	end
	updatePlayerLoadout(player)
	updateCurrencyEvent:FireClient(player, playerData.Stars) 
end
local function updatePlayerLoadout(player)
	local playerData = data[player.UserId]
	if playerData then
		local loadout = {}
		for i = 1, MAX_SELECTED_TOWERS do
			loadout[i] = playerData.SelectedTowers[i] or ""
		end
		ReplicatedStorage.UpdateLoadout:FireClient(player, loadout)
	end
end
local function updatePlayerData(player, itemName, action)
	local playerData = data[player.UserId]
	if action == "For Sale" then
		playerData.Stars -= towers[itemName].Price
		table.insert(playerData.OwnedTowers, itemName)
	elseif action == "Owned" then
		table.insert(playerData.SelectedTowers, itemName)
		if #playerData.SelectedTowers > MAX_SELECTED_TOWERS then
			table.remove(playerData.SelectedTowers, 1)
		end
	elseif action == "Equipped" then
		if #playerData.SelectedTowers > 1 then
			local towerToRemove = table.find(playerData.SelectedTowers, itemName)
			table.remove(playerData.SelectedTowers, towerToRemove)
		end
	end
	updatePlayerLoadout(player)
	updateCurrencyEvent:FireClient(player, playerData.Stars) 
end
local function updatePlayerLoadout(player)
	local playerData = data[player.UserId]
	if playerData then
		local loadout = {}
		for i = 1, MAX_SELECTED_TOWERS do
-- THIS PART HAS ERROR


			loadout[i] = playerData.SelectedTowers[i] or "" 


--THIS PART HAS ERROR
		end
		ReplicatedStorage.UpdateLoadout:FireClient(player, loadout)
	end
end
local function updatePlayerData(player, itemName, action)
	local playerData = data[player.UserId]
	if action == "For Sale" then
		playerData.Stars -= towers[itemName].Price
		table.insert(playerData.OwnedTowers, itemName)
	elseif action == "Owned" then
		table.insert(playerData.SelectedTowers, itemName)
		if #playerData.SelectedTowers > MAX_SELECTED_TOWERS then
			table.remove(playerData.SelectedTowers, 1)
		end
	elseif action == "Equipped" then
		if #playerData.SelectedTowers > 1 then
			local towerToRemove = table.find(playerData.SelectedTowers, itemName)
			table.remove(playerData.SelectedTowers, towerToRemove)
		end
	end
	updatePlayerLoadout(player)
	updateCurrencyEvent:FireClient(player, playerData.Stars) 
end
ReplicatedStorage.InteractItem.OnServerInvoke = function(player, itemName)
	local shopItem = towers[itemName]
	local playerData = data[player.UserId]
	if shopItem and playerData then
		local status = getItemStatus(player, itemName)

		if status == "For Sale" and shopItem.Price <= playerData.Stars then
			updatePlayerData(player, itemName, "For Sale")
		elseif status == "Owned" then
			updatePlayerData(player, itemName, "Owned")
		elseif status == "Equipped" then
			updatePlayerData(player, itemName, "Equipped")
		end

		return playerData
	else
		warn("Tower/player data does not exist")
	end

	return false
end

ReplicatedStorage.GetData.OnServerInvoke = function(player)
	return data[player.UserId]
end

Players.PlayerAdded:Connect(function(player)
	LoadData(player)
	updatePlayerLoadout(player)
end)

Here are the data errors I am getting in the GAME MATCH:

And here is the dataStore script:

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local database = DataStoreService:GetDataStore("database7")
local functions = ReplicatedStorage:WaitForChild("Functions")
-- tut
local getLoadoutFunction = functions:WaitForChild("GetLoadout")
-- tut
local getDataFunc = functions:WaitForChild("GetData")

local events = ReplicatedStorage:WaitForChild("Events")
local exitEvent = events:WaitForChild("ExitGame")


local MAX_SELECTED_TOWERS = 5

local data = {}

-- Load the players data
local function LoadData(player)
	local success = nil
	local playerData = nil
	local attempt = 2

	repeat
		success, playerData = pcall(function()
			return database:GetAsync(player.UserId)
		end)

		attempt += 1
		if not success then
			warn(playerData)
			task.wait()
		end

	until success or attempt == 3

	if success then
		print("Connection success")
		if not playerData then
			print("New player, giving default data")
			playerData = {
				["Stars"] = 200,
				["SelectedTowers"] = {"Pz. II"},
				["OwnedTowers"] = {"Pz. II", "M2 Light"}
			}
		end
		-- tut
		data[player.UserId] = playerData
		local stya = Instance.new("NumberValue",player)
		stya.Value = data[player.UserId].Stars
		stya.Name = "CombatXP"

-- THIS IS THE PART WITH THE ERROR


		for i, tower in pairs(data[player.UserId].SelectedTowers) do


-- THIS IS THE PART WITH THE ERROR
			local tower = ReplicatedStorage.Towers:FindFirstChild(tower)
			if tower then
				Instance.new("StringValue",player.SelectedTower).Name = tower.Name
			end
		end
		-- tut
	else
		warn("Unable to get data for player", player.UserId)
		player:Kick("There was a problem getting your data")
	end
	
end

Players.PlayerAdded:Connect(LoadData)

-- Save the players data
local function SaveData(player)
	if data[player.UserId] then
		local success = nil
		local playerData = nil
		local attempt = 1

		local info = workspace.Info
		local Stars = math.round(info.Wave.Value)


		if info.Message.Value == "VICTORY" and info.Mode.Value == "PettyWarfare" then
			Stars = 60

		elseif info.Message.Value == "VICTORY" and info.Mode.Value == "Normal" then
			Stars = 80

		elseif info.Message.Value == "VICTORY" and info.Mode.Value == "WorldWar" then
			Stars = 120

		end


		if info.Message.Value == "GAME OVER" and info.Mode.Value == "PettyWarfare" then
			Stars = 30			
		end

		if info.Message.Value == "GAME OVER" and info.Mode.Value == "Normal" then
			Stars = 45
		end

		if info.Message.Value == "GAME OVER" and info.Mode.Value == "WorldWar" then
			Stars = 60
		end
		
		data[player.UserId].Stars += Stars

		repeat
			success, playerData = pcall(function()
				return database:UpdateAsync(player.UserId, function()
					return data[player.UserId]
				end)
			end)

			attempt += 1
			if not success then
				warn(playerData)
				task.wait()
			end

		until success or attempt == 3

		if success then
			print("Data saved successfully")
		else
			warn("Unable to save data for", player.UserId)
		end
	else
		warn("No session data for", player.UserId)
	end

end
exitEvent.OnServerEvent:Connect(function(player)
	SaveData(player)
	data[player.UserId] = nil
end)

game:BindToClose(function()
	if not RunService:IsStudio() then
		for index, player in pairs(Players:GetPlayers()) do
			task.spawn(function()
				SaveData(player)
			end)
		end
	else
		print("Shutting down inside studio")
	end
end)


getDataFunc.OnServerInvoke = function(player)
	return data[player.UserId]
end

If you are still reading this message I am very thankful for you trying to help me and I will be very very very thankful if you help me with this data error because I have been trying to solve this for weeks without any fixes

2 Likes

I haven’t read through everything, but from what little I did read, there is alot of changes that need to be done.

For example, saving data anywhere else than the main datasave, this will cause inconsistencies with the datasaves and will potentially cause data to be loss from players. This is why leaderboards are often hard to get right because people use so many datastores with them, using more and more of the limit roblox has set on datastores.

Although I’m not sure this is causing your bug, if you have data save everytime you buy something, then you buy 100 of the same thing, it will surely cause dataloss if you leave right after. I would make the data save only after leaving and load only when joining, Also if you have a ton of datasaves for no reason, it will cause data loss aswell.

1 Like

Do you know some of the data stores I can delete without breaking the code?

2 Likes

If it’s a problem to begin with, it can be very hard to fix, I would personally just rewrite most of it.

1 Like

That would take even longer than searching how to fix this

2 Likes

Alright, looking at your datastore code, it says your selectedtowers is nil and your stars is nil, what this means is that it is loading the data but the data has nothing in it. so its either a problem with your save functions or a problem with the data grab function

1 Like

What do I look at (AI AI AI AI AI AI)

3 Likes

Apparent issue

After thoroughly investigating your code and eliminating potential causes, I have come to a single conclusion. Your existing data profile is corrupt or has an outdated schema.

What does this mean? A data schema is the expected shape of your data. This is largely outlined by your defaulting code, which expresses the following schema:

{
    Stars          : number,
    SelectedTowers : {number},
    OwnedTowers    : {number},
}

There are no flaws in your defaulting code, and I see no accidental overwrites in both of the provided scripts. The errors show that your data does exist, but certain properties of that data cannot be found. Your data persistence system lacks a reconciliation stage, which means this discrepancy is never repaired; there are no opportunities for existing data that does not match the expected schema to be properly flagged or recovered.

You have likely been making changes to your data persistence system over time, but have not switched or refreshed databases, or updated the data within them. I recommend those solutions, or to reset your current data

MISINFORMATION ## Another note

When a game server is shut down, the task scheduler no longer resumes threads. This means any form of interaction with coroutines will not function inside a game:BindToClose callback. You must remove your use of task.spawn

1 Like

That’s completely false, coroutines work and the Roblox documentation recommends using them with the BindToClose function to ensure that all the code is executed before the server closes.

1 Like

Strange. I recall this being an issue for quite some time. My tests support your rebuttal and I do see that the documentation makes full use of coroutines; it appears I am victim to the Mandela effect once again. I will correct this defect in my memory, lol

1 Like