Data Store + Minute Counter - Arithmetic problem

Good afternoon everyone! I’m attempting to make a script for the first time that keeps track of the minutes that a player has been in the game, and saves them in a datastore when they leave. However I’m getting an error about the arithmetic on line 44. Any ideas of why it won’t work?

local ds = game:GetService("DataStoreService"):GetDataStore("SaveData")

local joinTimes = {} -- store a table of when players joined

game.Players.PlayerAdded:Connect(function(plr)
	wait()
	local plrkey = "id_"..plr.userId
	local save1 = plr.leaderstats.Kills
	local save2 = plr.leaderstats.Minutes
	local save3 = plr.leaderstats.Deaths
	
	local GetSaved = ds:GetAsync(plrkey)
	if GetSaved then
		save1.Value = GetSaved[1]
		save2.Value = GetSaved[2]
		save3.Value = GetSaved[3]
	else 
		local NumberForSaving = {save1.Value, save2.Value, save3.Value}
		ds:GetAsync(plrkey, NumberForSaving)
	end
	
	local success, data = pcall(ds.GetAsync, ds, plrkey)
	
	if success then -- if it worked,
		save2.Value = data or 0 -- set it to whatever was saved (or 0 if nothing was saved)
		joinTimes[plr] = {data or 0, tick()} -- keep track of when the player joined and what they had saved
	else
		warn(data) -- put the error in the console as a warning
		
	end
end)

game.Players.PlayerRemoving:Connect(function(plr)
	ds:SetAsync("id_"..plr.userId, {plr.leaderstats.Kills.Value, plr.leaderstats.Minutes.Value, plr.leaderstats.Deaths.Value})
end)

game:GetService("RunService").Heartbeat:Connect(function()
	-- every frame, for every player, update their time played
	local now = tick()
	for plr, times in pairs(joinTimes) do
		local leaderstats = plr:FindFirstChild("leaderstats")
		if leaderstats and leaderstats:FindFirstChild("Minutes") then
			leaderstats["Minutes"].Value = times[1] + math.floor((now - times[2]) / 60)
			-- set their time played to what was saved + how long it's been since they've joined (in min)
		end
	end
end)

Are you able to supply a proper code sample? I was unable to format this even in VS Code because it wasn’t posted properly. That would help see some issues with the code and be able to work with it better. Tip for including code in your thread: put them in a codeblock.

```lua
Paste your code here.
```

1 Like

Thanks, hopefully it’s easier for you to read now-

Appreciate it! It’s much easier to read and work with now.

I think you do have a few misunderstandings of how DataStores work, as well as some bad practice problems to resolve in your code. Remember that you have the Developer Hub for reference on any API you might not know how to use. Additionally, Line 44 to me appears as a comment, so that might not be accurate of where your arithmetic error is, rather it seems Line 43 is the problem.

Here’s a jot list of the practice problems in your code that you can resolve:

  • Try to use clear variable names. DataStore is clearer than ds.

  • You’ll want to keep your PlayerAdded and PlayerRemoving functions in variables and connect them later. This will be so you can enforce code readability and make sure to account for players already in the server before this code runs.

  • There’s no reason to use wait in PlayerAdded. If this is to wait for the leaderstats to get created first, I suggest that you merge the two scripts. All data handling is best done in one script or a system that allows you to easily work with data without needing to worry about race conditions (this code running before leaderstats are added, for example).

  • One of your GetAsync calls passes a table value as well. Did you perhaps mean to use SetAsync here instead to set default values for player data? I’m thinking of this mainly because of the way the code is written which seems to suggest you wanted to write defaults.

  • You have an inconsistent use of pcall for DataStore methods. Any ideal code working with DataStores should always feature pcalls for all method calls.

  • It might be more ideal for you to store player data in a dictionary so you can be very clear about what you’re fetching. It’s more clear if you want kills to use Data.Kills rather than Data[1].

  • Remember to remove the player from your joinTimes dictionary when they leave! If you fail to do that, you will encounter a memory leak. The player instance will persist in the game and won’t be cleaned up because this table still holds a reference to the object.

I have a bit of a code cleanup suggestion that you can use which incorporates the above feedback. It’s still pretty much your same code but with differences. Please do note, the following assumptions are being made about your code:

  • You’ll be using leaderstats to store data. Typically not recommended and that you should only use them to display data for built-in leaderboards.

  • Your data will always be one-dimensional, meaning there will always just be values in the folder but there will be no other folders or objects either in leaderstats or under the values.

  • You will only ever use numerical values (integers specifically) for your data.

  • This is not necessarily a whole and complete production-ready code sample. There are, in itself, a lot of things that I have intentionally or mistakenly left out, such as detailed handling of unexpected cases (e.g. data won’t save if leaderstats get deleted, I don’t account for potentially new or old values, so on).

  • I don’t use UpdateAsync, even though I recommend that when trying to update data.

  • This is pretty crude of an example.

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

local SaveDataStore = DataStoreService:GetDataStore("SaveData")
local TimeTracker = {}

local STATS_TO_CREATE = {
    "Minutes", "Kills", "Deaths"
}

local CORRUPTION_FLAG_NAME = "DidNotLoad"

--- This will help prepare and convert data for saving.
-- Quick utility function I usually pull around.
local function folderToDictionary(folder)
    local dictionary = {}

    for _, child in ipairs(folder:GetChildren()) do
        if child:IsA("Folder") then
            dictionary[tostring(child.Name)] = folderToDictionary(child)
        elseif child:IsA("ValueBase") then
            dictionary[tostring(child.Name)] = child.Value
        end
    end

    return dictionary
end

local function playerAdded(player)
    local playerKey = "id_" .. player.UserId

    local leaderstats = Instance.new("Folder")
    leaderstats.Name = "leaderstats"

    for _, statName in ipairs(STATS_TO_CREATE) do
        local stat = Instance.new("IntValue")
        stat.Name = statName
        stat.Parent = leaderstats
    end

    local success, result = pcall(function ()
        return SaveDataStore:GetAsync(playerKey)
    end)

    if success then
        if result then
            for key, value in pairs(result) do
                local statObject = leaderstats:FindFirstChild(key)
                if statObject then
                    statObject.Value = value
                end
            end
        end
    else
        warn(player.Name .. "'s data could not be loaded due to an error: " .. result)
        CollectionService:AddTag(leaderstats, CORRUPTION_FLAG_NAME)
    end

    TimeTracker[player] = os.clock()

    leaderstats.Parent = player
end

local function playerRemoving(player)
    local playerKey = "id_" .. player.UserId
    local leaderstats = player:FindFirstChild("leaderstats")

    if leaderstats and not CollectionService:HasTag(leaderstats, CORRUPTION_FLAG_NAME) then
        local data = folderToDictionary(leaderstats)
        SaveDataStore:SetAsync(playerKey, data)
    end

    TimeTracker[player] = nil
end

-- This might be a bit expensive primarily because of the FindFirstChilds
local function updateMinutesPlayed()
    local currentTime = os.clock()

    for player, timeInServer in pairs(TimeTracker) do
        local leaderstats = player:FindFirstChild("leaderstats")
        local minutes = leaderstats and leaderstats:FindFirstChild("Minutes")
        if leaderstats and minutes then
            local timeElapsedDecimal = (currentTime - timeInServer)/60
            if timeElapsedDecimal >= 1 then
                minutes.Value = minutes.Value + 1
                TimeTracker[player] = currentTime
            end
        end
    end
end

Players.PlayerAdded:Connect(playerAdded)
Players.PlayerRemoving:Connect(playerRemoving)
RunService.Heartbeat:Connect(updateMinutesPlayed)

for _, player in ipairs(Players:GetPlayers()) do
    playerAdded(player)
end
3 Likes

Wow, I appreciate the thorough answer! I admit that I’ve only been learning about data stores for a week, and my code is just me putting everything together in hopes that it would work. I have read tutorials but I’m still not very good at solving errors with, or optimizing my code.

I appreciate a thorough answer so that I can really study what I did wrong, and hopefully be able to improve and understand more of how to create data stores that don’t leak memory, or lose data; also to make my code overall more efficient.

So again, thanks for the help!