Why is 'data' nil?

I am creating a datastore module. However, one of the functions error every test. I’m assuming it is DataGuard:GetData() but I am not sure.

Module Code:

local metaServerData = {
	ServerData = {};
}
local DataGuard = setmetatable({}, {__index = metaServerData})
local hs = game:GetService("HttpService")
local dss = game:GetService("DataStoreService")
local ds = dss:GetDataStore("Data")
local sessionLocks = dss:GetDataStore("SessionLocked")

type Data = {}
type PlayerName = {}

if not script:WaitForChild("Save Structure", 2) then local ss = Instance.new("Folder") ss.Name = "Save Structure" ss.Parent = script end

local function SessionLock(player: Player)
	local locked
	sessionLocks:UpdateAsync(tostring(player.UserId), function(isLocked)
		locked = isLocked
		return true
	end)
	return locked
end

local function UnsessionLock(player: Player)
	sessionLocks:UpdateAsync(tostring(player.UserId), function(isLocked)
		return false
	end)
end

local function createEvent(eventName)
	local event = Instance.new("BindableEvent")
	event.Name = eventName
	return event.Event, event
end

function DataGuard:GetData(player: Player | PlayerName)
	if not player then Warn("Missing player.") return end
	if type(player) ~= "string" then if not player:IsA("Player") then return end end
	local plr = type(player) == "string" and game.Players:FindFirstChild(player) or player
	if not DataGuard.ServerData[plr] then return end
	return DataGuard.ServerData[plr]
end

local function playerAdded(player: Player)
	if DataGuard.ServerData[player] then return end
	local data = {}
	if not script["Save Structure"]:GetChildren()[1] then Warn("No save structure provided!") return end
	for _, v in script["Save Structure"]:GetChildren() do
		if not v:IsA("ValueBase") then continue end
		data[v.Name] = v.Value
	end
	local metadata = {__metatable = "You can just see what's inside the metatable via the code..."; StatsStorage = {}}
	
	local changedEvent, bindable = createEvent("Changed")
	metadata.Changed = changedEvent
	
	function metadata:Set(Key: string, NewValue: any)
		if SessionLock(player) then return end
		if Key == nil or NewValue == nil then Warn("Value missing!") return end
		if not metadata.StatsStorage[Key] then Warn("Could not find", Key, "in", player.Name.."'s", "DataStore!") return end
		metadata.StatsStorage[Key] = NewValue
		bindable:Fire(Key, NewValue)
	end
	
	function metadata:Save()
		if SessionLock(player) then return end
		local success = false
		for attempts = 1, 5 do
			success = pcall(function()
				ds:SetAsync(tostring(player.UserId), hs:JSONEncode(data))
			end)
			if success then break end
		end
		if not success then Warn("Could not save", player.Name.."'s data!") end
	end
	
	function metadata:GetLastSave(): Data
		local returned
		pcall(function()
			local saved = ds:GetAsync(tostring(player.UserId))
			if saved then
				returned = hs:JSONDecode(saved)
			else
				returned = false
			end
		end)
		return
	end
	
	function metadata:GetValue(Key: string): any
		if Key == nil then Warn("Value missing!") return end
		return metadata.StatsStorage[Key]
	end
	
	metadata.__index = metadata
	metadata.__tostring = function(t)
		if t ~= data then return t end
		local s = "{"
		for i, v in t do
			s = s..v
			if i ~= #t then
				s = s..", "
			end
		end
		s = s.."}"
		return s
	end
	
	data = metadata:GetLastSave() or data
	for k, v in data do
		metadata.StatsStorage[k] = v
	end
	data = {}
	setmetatable(data, metadata)
	DataGuard.ServerData[player] = data
end

for _, v in game.Players:GetPlayers() do
	playerAdded(v)
end
game.Players.PlayerAdded:Connect(playerAdded)
game.Players.PlayerRemoving:Connect(function(player)
	DataGuard.ServerData[player]:Save()
	UnsessionLock(player)
	DataGuard.ServerData[player] = nil
end)
game:BindToClose(function()
	for _, v in game.Players:GetPlayers() do
		DataGuard.ServerData[v]:Save()
	end
	task.wait(5)
end)
	
function Warn(...)
	warn("DataGuard:", ...)
end

local elapsed = 0
game:GetService("RunService").Heartbeat:Connect(function(delta)
	elapsed = math.clamp(elapsed + delta, 0, 60)
	if elapsed ~= 60 then return end
	elapsed = 0
	if not DataGuard.AutoSaveEnabled then return end
	for _, v in game.Players:GetPlayers() do
		DataGuard.ServerData[v]:Save()
	end
end)

return DataGuard

Test Script Code:

local DataGuard = require(script.Parent:WaitForChild("DataGuard"))

game.Players.PlayerAdded:Connect(function(player)
    local data
    repeat data = DataGuard:GetData(player) task.wait() until DataGuard:GetData(player)
    print(data)
    print(getmetatable(data))
    task.wait(0.09)
    local leaderstats = Instance.new("Folder")
    leaderstats.Name = "leaderstats"
    local kills = Instance.new("IntValue")
    kills.Name = "Kills"
    kills.Value = data:GetValue("Kills")
    kills.Parent = leaderstats
    local wins = Instance.new("IntValue")
    wins.Name = "Wins"
    wins.Value = data:GetValue("Wins")
    wins.Parent = leaderstats
    leaderstats.Parent = player
    data.Changed:Connect(function(value, newvalue)
        print(value, newvalue)
        leaderstats[value].Value = newvalue
    end)
    player.Chatted:Connect(function(message)
        if message:lower() == "kill" then
            data:Set("Kills", data:GetValue("Kills") + 1)
        elseif message:lower() == "win" then
            data:Set("Wins", data:GetValue("Wins") + 1)
        end
    end)
end)

4 Likes

The GetData() function is returning nil because when it is called, the data for the respective player has not been created and stored yet. This is due to the asynchronous nature of datastore operations.

The function playerAdded() where the player’s data is created and stored in ServerData table is only run when PlayerAdded event is fired. However, in your test script, GetData() is being called immediately after PlayerAdded event fires, but there isn’t any guarantee that playerAdded() has fully run and stored the data into DataGuard.ServerData at this point.

To solve this, you need to make sure that the player’s data is fully created and stored before you attempt to fetch it. One quick solution for this could be to add a Yield function such as wait(), task.wait(), or checking whether the data is available yet before fetching it:

The following code uses a repeat loop to continuously try and get the data until it is available:

repeat data = DataGuard:GetData(player) task.wait() until DataGuard:GetData(player)

This will keep attempting to acquire the player’s data, pausing the script for a short duration in between each attempt until it succeeds. This will ensure that you attempt to retrieve the data after it has been created by the playerAdded() function.

Remember that this solution, although solves your problem, it is generally not the best practice to use repeat-wait loops as they can lead to infinite loops if the condition is never met.

A better approach would be to modify and organize your code to ensure that you’re not trying to fetch the player’s data before it’s ready. You might modify your module to include some kind of event or callback to signal when the data is ready for each player, and then use that in your test script to know when to call GetData().

This approach can feel a little complex for beginners but this would be the proper way to handle such situations.

5 Likes

This sounds like ChatGPT made this response.

4 Likes

Maybe instead of

you can try

local data = DataGuard:GetData(player)
if data then
    --the rest of the code
end  

I’m not entirely sure if thats the main issue, but I have to leave soon. But when i get back i can try to find a better way if it does not work.

4 Likes

I’m assuming the error is that data has not been loaded yet, but the repeat loop is there to prevent that.

2 Likes

Yeah, you’re right. The error is probably because the data hasn’t been loaded yet. The repeat loop is there to keep checking until the data is loaded. But it seems like the repeat loop is not working as expected. I would still advise you to be somewhat respectful (I did not use ChatGPT to write this reply) and give me some sort of coherent answer so we can push back further.

2 Likes

I can’t really tell you anything further as your initial reply didn’t really give me anything to work with.

What made you think I was a beginner?

Oh and also, I’m not trying to be rude.

4 Likes

Oh whoops, didn’t mean to assume you’re a beginner, and didn’t wanna come off as rude either. I just wanted to make sure the info was accessible to all skill levels. My bad

So, to add more substance to what I mentioned before: Your module runs the playerAdded() function when a player joins, right? But your test script is running the GetData() function pretty much instantly after a player arrives. Thing is, there might not be enough time for the playerAdded() function to do its job and store data before GetData() runs. The datastore operations are async, meaning they don’t always happen in order.

If the problem isn’t that though, another possible issue is with the way you’re distinguishing types of ‘player’ inputs in the GetData() function. You could try debugging whether the input is recognized properly and whether the function navigates the if-statements correctly.

So, my question would be: can you tell me where and how you’re seeing that ‘data’ is nil?

2 Likes

I found the solution (I don’t understand why this works).

repeat data = DataGuard:GetData(player) task.wait() until data ~= nil

I guess it wasn’t the module code that was wrong.

2 Likes

I realise how much effort you take in to write these replies, so sorry about that.

3 Likes

No worries, glad you figured it out! The reason why your fix works might be because the repeat-until loop now explicitly checks if the data is not nil before proceeding. Previously it was just checking if GetData() returns anything. Now it waits until it receives a non-nil response.

2 Likes

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