Datastore script not working

I was making a datastore script for @ProvenData and for some reason the data isn’t saving. Here is the script:

local module = {}
local dataStoreService = game:GetService("DataStoreService")
local dataStore = dataStoreService:GetDataStore("PlayerData")
local err = warn
local playerData = {}
local defaultDataID = 0

function loadData(player)
	local key = "key-"..player.UserId
	
	local data = nil
	local success, errorDebug = pcall(function()
		data = dataStore:GetAsync(key)
	end)
	
	if success then
		if data then
			table.insert(playerData, data)
		else
			data = {defaultDataID, player.Name}
			table.insert(playerData, data)
		end
	else
		err(errorDebug)
	end
end

function saveData(player)
	local key = "key-"..player.UserId
	
	local data = nil
	local index = 0
	
	for i, v in ipairs(playerData) do
		if v[2] == player.Name then
			data = v
			index = i
			break
		end
	end
	print(1) -- So I can see where the script is stopping at
	if data then
        print(2) -- So I can see where the script is stopping at
		local success, errorMessage = pcall(function()
			print(3) -- So I can see where the script is stopping at
			dataStore:UpdateAsync(key, function(oldData)
                print(4) -- So I can see where the script is stopping at
				local previousData = oldData or {defaultDataID, player.Name}
				
				if data[1] == previousData[1] then
					data[1] = data[1] + 1
					return data
				else
					return nil
				end
			end)
            print(5) -- So I can see where the script is stopping at
		end)
        print(6) -- So I can see where the script is stopping at
		table.remove(playerData, index)
		
		if success then
			print("Data successfully saved")
		else
			err(errorMessage)
		end
	end
end

game.Players.PlayerAdded:Connect(loadData)

game.Players.PlayerRemoving:Connect(saveData)

--[[game:BindToClose(function()
	for i, v in next, game:GetService("Players"):GetChildren() do
		coroutine.wrap(function()
			saveData(v)
		end)
	end
end)]]--

for i, v in next, game:GetService("Players"):GetChildren() do
	coroutine.wrap(function()
		loadData(v)
	end)()
end

return module

I’ve also added prints to see where the script gets stops at.

For some reason, when I leave the game it only prints 1, 2, and 3 then it stops. Can someone please tell me why the data isn’t saving, and how to fix the issue?

EDIT: Also, here is how the data I’m trying to save looks like: {0, Username, "Random string"}. 0 Is how many times the player has left the game, Username is self explanatory, and the string at the end of the table is the playerdata (It’s to test if the datastore script works).

From what I’ve read I assume you are trying to save data when leaving the game? In that case, you could use Players.PlayerRemoving.

Thats what I’m doing, and it’s not working.

Did you read the script lol?

I noticed you set local data to nil when saving your data? Is that intentional?

My bad, I’m misreading a lot of things.

I set data to nil, but then I try to find the player data associated with the leaving player, then when that data is found, we set local data to be equal to the player data associated with the leaving player then we break out of the loop.

What shows in the output? It sometimes helps to see what’s going on there.

Here is what it does…

Shouldn’t you first check if Old Data has been received and is true? Therefore adding an additional if statement above.

Boy mobile isn’t helping during my vacation.

I put an or after it, and then I put in this {defaultDataID, player.Name}. So if oldData happens to be nil roblox automatically uses the optional data {defaultDataID, player.Name}.

1 Like

Have you considered trying to use the lua debugger? Set a breakpoint at the top of saveData and step through the code, line-by-line, inspecting your variabels and seeing if they are what you expect.

1 Like

I tried to simplify your code a bit, and maybe fixed a bug or two. Check the -- @nicemike40 comments for my thoughts. No guarantees that this works, but it should be easier to reason about:

local module = {}

local dataStoreService = game:GetService("DataStoreService")
local dataStore = dataStoreService:GetDataStore("PlayerData")
local err = warn

-- @nicemike40 using a weak-keyed map from player objects to data so that
-- the entry is cleaned up if you leave the game
local playerData = setmetatable({}, {__mode = "k"})
local defaultDataID = 0

function loadData(player)
    local key = "key-"..player.UserId
    
    local data = nil
    local success, errorDebug = pcall(function()
        data = dataStore:GetAsync(key)
    end)
    
    if success then
        -- @nicemike40 just a simplification
        playerData[player] = data or {defaultDataID, player.Name}
    else
        err(errorDebug)
    end
end

function saveData(player)
    local key = "key-"..player.UserId
    
    local data = playerData[player]
    
    print(1) -- So I can see where the script is stopping at
    if data then
        print(2) -- So I can see where the script is stopping at
        local success, errorMessage = pcall(function()
            print(3) -- So I can see where the script is stopping at
            dataStore:UpdateAsync(key, function(oldData)
                print(4) -- So I can see where the script is stopping at
                
                -- @nicemike40 I didn't understand your logic in this function,
                -- why would you ever want to return nil? I simplified it:
                if oldData then data[1] = oldData[1] + 1 end
                
                return data
            end)
            print(5) -- So I can see where the script is stopping at
        end)
        
        print(6) -- So I can see where the script is stopping at
        
        -- @nicemike40 map is weak, so the entry is removed from the map only if
        -- the player object is actually destroyed
        
        if success then
            print("Data successfully saved")
        else
            err(errorMessage)
        end
    end
end

game.Players.PlayerAdded:Connect(loadData)

game.Players.PlayerRemoving:Connect(saveData)

-- @nicemike40 see my comment below
--[[game:BindToClose(function()
for i, v in next, game:GetService("Players"):GetChildren() do
coroutine.wrap(function()
saveData(v)
end)
end
end)]]--

-- don't use next lol
for _, player in pairs(game:GetService("Players"):GetPlayers()) do
    -- @nicemike40 hmmm... be careful here. I don't think it's a problem, but
    -- you should be aware that you're calling a yielding function (loadData)
    -- inside this coroutine, which means that if a player leaves while you're
    -- still loading their data, you might have a data race as saveData tries
    -- to access the same player data while loadData is still running.
    --
    -- The same issue might just happen normally with PlayerRemoving/
    -- PlayerAdded, but I'm not sure those run in separate coroutines so 
    -- the problem is less pronounced.
    coroutine.wrap(function()
        loadData(player)
    end)()
end

return module
1 Like

@nicemike40 So I happened to fix the issue by using this modulescript:

local dataModule = {}
local err = warn

function dataModule:save(key, data, dataStore, defaultOldData, typeOf)
	if data then
		local success, errorDebug = pcall(function()
			
			dataStore:UpdateAsync(key, function(oldData)
				
				local previousDataId-- = oldData[1] or oldData.DataId or defaultOldData
				
				if oldData then
					previousDataId = oldData[1]
				else
					if oldData then
						if not oldData[1] then
							if oldData.DataId then
								previousDataId = oldData.DataId
							end
						end
					end
				end
				
				if not previousDataId then
					previousDataId = defaultOldData[1] or defaultOldData.DataId
				end
				
				local currentDataId = data[1]
				
				local where = "A"
				
				if not currentDataId then
					
					where = "B"
					
					currentDataId = data.DataId
						
				end
				
				if not currentDataId then
					
					warn("Could not save data. Please go to game.ServerScriptService.DataModule, and read the comments at the top of the script!")
					
					return nil
				end
				
				
				if previousDataId == currentDataId then
					
					if where == "A" then
						
						data[1] = data[1] + 1
							
					end
					
					if where == "B" then
						
						data.DataId = data.DataId + 1
						
					end
					
					return data
				else
					
					return nil
				end				
			end)
		end)
		
		if success then 
			print("Successfully saved data of type: "..typeOf)
		else
			err(errorDebug)
		end
		return success, errorDebug
	end
end

function dataModule:load(key, dataStore)
	local data = nil
	local success, errorDebug = pcall(function()
		data = dataStore:GetAsync(key)
	end)
	
	if success then
	else
		err(errorDebug)
	end
	return {["success"] = success, ["errorDebug"] = errorDebug, ["data"] = data}
end

return dataModule

Now, to answer your questions on why I return nil, it’s because if the script detects that the data could not load, it will return nil thus not saving the data.

So basically, if I had a million cash in the game, and the script couldn’t load my cash, the loaded cash would be 0, so if I would leave the game it would override the million cash with 0, thus causing dataloss. Thats why we return nil if the data could not load. Because update async doesn’t save the data if we return nil.

-- don't use next lol

Also, why should I not use next lol?

-- @nicemike40 using a weak-keyed map from player objects to data so that
-- the entry is cleaned up if you leave the game
setmetatable({}, {__mode = "k"})

Where should I go, to learn more about metatables?