How to avoid creating a pointer to a table?

Thanks for reading this in advance.

I’m fiddling with a data-persistence module that interfaces with the DataStoreService, and I’m running into a befuddling issue: when I run the game in a two-player test server, any changes made to one player’s data are replicated in the other; If Player1 gets a KO, Player2 gets a KO, etc. After banging my head against the wall for a couple hours, I’m convinced I’ve managed to track down the source, a single function in my module:

local function setupPlayerData(player)
	local pID = "Player_" .. player.UserId
	local success, data, oldData
	
	success, data = dataRetry(function()
		return pData:GetAsync(pID)
	end)
	
	success, oldData = dataRetry(function()
		return pData:GetAsync(player.UserId)
	end)
	
	if success then
		if data then sessionData[pID] = data
		elseif oldData then sessionData[pID] = convertOld(data)
		else sessionData[pID] = {Game = game_Stats, Class = setmetatable(class_Stats, mt)}
		end
	else warn("Cannot access data store for player!") 
	end
end

If the datastore is functioning, but the player doesn’t have any data, it creates an entry for the player in sessionData and sets it to a table containing two other tables, one being the default game data and the other being a list of data relevant to every playable class in the game. The two tables - game_stats and class_Stats - are global variables listed at the top of the script, which I’ll show you here:

i_n = Instance.new
c3 = Color3.new
uit = Enum.UserInputType
kc = Enum.KeyCode

--
function wfc(p, o, t)
	return p:WaitForChild(o, t)
end

function newC3(r, g, b)
	return c3(r/255, g/255, b/255)
end

--
local ss = game:GetService("ServerStorage")
local cBin = wfc(ss, "Classes")

local dss = game:GetService("DataStoreService")
local pData = dss:GetDataStore("PSATest")

local manager = {}
local sessionData = {}
local class_Stats = {}
local gData = {}

local autosave_int = 300

--
for _,p in pairs(cBin:GetChildren()) do
--
	local t = {
		Wins = 0,
		Rounds = 0,
		KOs = 0,
		WOs = 0,
		KDR = 0,
		WR = 0,
		Damage = 0,
		Healing = 0,
		Time = 0,
		Skin = "",
		PrimaryColor = newC3(91, 93, 105),
		SecondaryColor = newC3(159, 161, 172),
		TertiaryColor = newC3(248, 248, 248),
	}
	
	local g = {
		KOs = 0,
		WOs = 0,
		Rounds = 0
	}
	
--
class_Stats[p.Name] = t
gData[p.Name] = g
end

local game_Stats = {
	
	--
	Stats = {
		Points = 0,
		Rounds = 0,
		Wins = 0,
		Streak = 0,
		KOs = 0,
		WOs = 0,
		KDR = 0,
		WR = 0,
		Damage = 0,
		Healing = 0,
		Time = 0,
		LastPlayed = os.date("*t"),
		Joined = os.date("*t"),
	},
	
	--
	Settings = {
		Class = "Warrior",
		Playing = true, ToolMode = false,
		OverheadHP = false, SmallUI = false,
		ShowHits = false, Volume = 50
	},
	
	--
	KeyBinds = {
		ATK = {uit.MouseButton1, kc.Unknown},
		AB1 = {uit.Keyboard, kc.Q},
		AB2 = {uit.Keyboard, kc.E},
		CRT = {uit.Keyboard, kc.F}
	},
	
	PadBinds = {
		ATK = {uit.Gamepad1, kc.ButtonR2},
		AB1 = {uit.Gamepad1, kc.ButtonX},
		AB2 = {uit.Gamepad1, kc.ButtonY},
		CRT = {uit.Gamepad1, kc.ButtonB}
	},
	
	--
	Inventory = {}
	
}

local mt = {
	__call = function(stats, class)
		stats = stats[class]
		if stats then gData.KOs = gData.KOs + stats.KOs
			gData.WOs = gData.WOs + stats.WOs
			gData.Rounds = gData.Rounds + stats.Rounds
		end
	end
}

Thus, I’m assuming changes to individual players’ data are replicated to the others because they technically don’t have their own data tables, just a pointer to the global tables I defined at the top of the data script. I’ve tried localizing them to the function instead, to no avail. If this is in fact the issue, how can I give new players a copy of the default data tables without simply making a pointer to them?

2 Likes

You’ll have to manually copy the default tables. There’s several ways to do this, but the best one for your use-case is this:

function deepcopy(orig)
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepcopy(orig_key)] = deepcopy(orig_value)
        end
        setmetatable(copy, deepcopy(getmetatable(orig)))
    else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end

You can pass a table (like your game_stats table) as an argument to this function, and it will return an identical table.

2 Likes

The code you provided won’t work for the recursive tables that @Geomaster uses.
On the wikipage you provided it gives a second code that does works with recursive tables.

function deepcopy(orig, copies) -- You only need to provide the table, don't worry about the copies argument.
    copies = copies or {}
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        if copies[orig] then
            copy = copies[orig]
        else
            copy = {}
            for orig_key, orig_value in next, orig, nil do
                copy[deepcopy(orig_key, copies)] = deepcopy(orig_value, copies)
            end
            copies[orig] = copy
            setmetatable(copy, deepcopy(getmetatable(orig), copies))
        end
    else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end
1 Like

A recursive table is a table that references itself. For example:

local t = {7,3,4,2}
t.self = t
1 Like