Unintentional shared datastores between freshly joined players and players already in the server

I’m currently working on a simple mining game just for fun. It’s inherently based on a kit someone else made and I’ve been able to work with it for a long time, however there’s been a glaring issue with datastores since the beginning that I can’t find the root cause of and forces me to make my game solo just for new players.

Problem
The problem seems to be that when a completely new player (so, probably without a datastore) joins another player, their inventory becomes shared with the other until the new player leaves and rejoins. This can cause two or more people to end up with the same inventory.

I have tried looking into the situation myself on numerous occasions as well as searching up this issue, but I still can’t exactly figure out the problem. I know there’s two major scripts that handle the early stages of datastore functionality which I will send shortly. I’m hoping someone can figure out where the source of the issue is stemming from, so I’ll send the entire scripts below. Sorry if it’s too wall-of-text-y.

Note: I want to emphasize that the problem seems to go away when the players rejoin. Multiplayer works fine assuming all players already have joined a solo player beforehand and mined a few things. Unfortunately this also doesn’t seem to show any related errors when it occurs. This game does send errors but it’s mostly just cosmetic fluff…

The “Data” script found in ServerScriptService. Seems to handle the initialization and core of the datastores.

-- Data script | oofblocks 5/25/21

local replicatedStorage = game:GetService("ReplicatedStorage")
local serverStorage = game:GetService("ServerStorage")
local dataStoreService = game:GetService("DataStoreService")
local players = game:GetService("Players")

local store = dataStoreService:GetDataStore("UniverseTestingData")

local config = require(workspace.Configuration)

local cache = {}

local dataTemplate = {
	Inventory = {};
	Pickaxes = {};
	Pickaxe = "Default Pickaxe";
	OreMined = 0;
	Level = 1
}

for _, data in pairs(config.Alts) do
	dataTemplate[data.Name .. "Inventory"] = {}
end

local function getData(plr)
	if cache[plr.Name] then
		return cache[plr.Name]
	end
	local success, data = pcall(function()
		return store:GetAsync(plr.UserId)
	end)
	if success then
		cache[plr.Name] = data or dataTemplate
		return cache[plr.Name]
	end
	cache[plr.Name] = dataTemplate
	return cache[plr.Name]
end

replicatedStorage.GetData.OnServerInvoke = getData
serverStorage.GetDataAgain.OnInvoke = getData

local function added(plr)
	local data = getData(plr)
	
	for key, value in pairs(dataTemplate) do
		if not data[key] then
			data[key] = value
		end
	end
	
	for _, ore in pairs(replicatedStorage.Ores:GetChildren()) do
		repeat wait() until ore
		for k, v in pairs(data) do
			if string.sub(k,-9) == "Inventory" then
				v[ore.Name] = v[ore.Name] or 0
			end
		end
	end
	
	for _, pickaxe in pairs(replicatedStorage.Pickaxes:GetChildren()) do
		if pickaxe.Name ~= "Default Pickaxe" then
			data.Pickaxes[pickaxe.Name] = data.Pickaxes[pickaxe.Name] or false
		else
			data.Pickaxes["Default Pickaxe"] = true
		end	
	end
	
	local function createVal(class, name, val)
		local v = Instance.new(class)
		v.Name = name
		v.Value = val
		v.Parent = plr
		return v
	end
	
	createVal("IntValue", "Level", data.Level)
	createVal("IntValue", "OreMined", data.OreMined)
	createVal("StringValue", "Pickaxe", data.Pickaxe)
	
	cache[plr.Name] = data
end

players.PlayerAdded:Connect(added)

local function save(plr)
	local data = getData(plr)
	
	data.Level = plr.Level.Value
	data.OreMined = plr.OreMined.Value
	data.Pickaxe = plr.Pickaxe.Value
	
	local success, err = pcall(function()
		store:SetAsync(plr.UserId, data)
	end)
	
	if not success then
		warn(string.format("Error saving data for %s: %s", plr.UserId, err))
	else
		print(string.format("Successfully saved %s's data", plr.UserId))
	end
end

players.PlayerRemoving:Connect(save)

local function saveAll()
	for _, plr in pairs(players:GetPlayers()) do
		save(plr)
	end
end

game:BindToClose(saveAll)

coroutine.wrap(function()
	while wait(120) do
		saveAll()
	end 
end)()

local function awardOre(plr, ore, amount, alt)
	local data = getData(plr)
	local tab = data["Inventory"]
	if alt then
		if alt == 1 then
			tab = data["FunkyInventory"]
		elseif alt == 2 then
			tab = data["SinisterInventory"]
		elseif alt == 3 then
			tab = data["MutilatedInventory"]
		else
			tab = data[alt .. "Inventory"]
		end
		if not tab then
			return warn(string.format("Altered type '%s' does not exist!", alt))
		end
	end
	if tab[ore] then
		tab[ore] += amount
		replicatedStorage.UpdateInventory:FireClient(plr, data)
		return
	end
	return warn(string.format("Ore %s does not exist!", ore))
end

serverStorage.AwardOre.OnInvoke = awardOre

local replicatedStorage = game:GetService("ReplicatedStorage")
local serverStorage = game:GetService("ServerStorage")

local function haveit(plr,Item)
	local data = getData(plr)
	if data.Inventory[Item] > 0 or
		data.FunkyInventory[Item] > 0 or
		data.SinisterInventory[Item] > 0 or
		data.MutilatedInventory[Item] > 0
	then
		return true
	else
		return false
	end
end

serverStorage.Haveit.OnInvoke = haveit

local function craftPickaxe(plr, pickaxe)
	local data = getData(plr)
	if data then
		local realPickaxe = replicatedStorage.Pickaxes:FindFirstChild(pickaxe)
		local items = realPickaxe.Cost:GetChildren()

		local score = 0
		for _, v in pairs(items) do
			if
				data.Inventory[v.Name] >= v.Value or 
				data.FunkyInventory[v.Name] >= v.Value or
				data.SinisterInventory[v.Name] >= v.Value or
				data.MutilatedInventory[v.Name] >= v.Value
			then
				score += 1
			end
		end
		if score == #items then
			plr.Pickaxe.Value = pickaxe
			data.Pickaxes[pickaxe] = true
			return true
		end
		return false
	end
	return false
end

replicatedStorage.CraftPickaxe.OnServerInvoke = craftPickaxe

local function equipPickaxe(plr, pickaxe)
	local data = getData(plr)
	if data.Pickaxes[pickaxe] then
		plr.Pickaxe.Value = pickaxe
	end
end

replicatedStorage.EquipPickaxe.OnServerEvent:Connect(equipPickaxe)

The start of the Main file, in the same location. Just in-case this is causing the issue too. I tried commenting out the data lines here but it seems like an important failsafe.

-- Main handler for the game | oofblocks 5/24/21

local replicatedStorage = game:GetService("ReplicatedStorage")
local serverStorage = game:GetService("ServerStorage")
local http = game:GetService("HttpService")
local players = game:GetService("Players")

local config = require(workspace.Configuration)
local altify = require(replicatedStorage.Altify)

-- important
workspace.Regen.Position = Vector3.new(math.floor(workspace.Regen.Position.X),math.floor(workspace.Regen.Position.Y),math.floor(workspace.Regen.Position.Z))

local regenPos, regenCF = workspace.Regen.Position, workspace.Regen.CFrame

for _, v in pairs(workspace.Plugins:GetChildren()) do
	if v:IsA("ModuleScript") then
		require(v)()
	end
end

local function added(p)
	if table.find(config.Banned, p.Name) or table.find(config.Banned, p.UserId) then
		p:Kick("banned nerd")
	end

	local data = serverStorage.GetDataAgain:Invoke(p)
	
	replicatedStorage.UpdateInventory:FireClient(p, data)

	if config.OrePlacer then
		local placer = serverStorage["ore placer"]:Clone()
		placer.Parent = p:WaitForChild("Backpack")
	end
end

players.PlayerAdded:Connect(added)

Does the script check id of player?if not then this can be solution im not scripter

I have tried reworking the script around using player name and player ID many times. It doesn’t change the outcome whatsoever.

i believe this is the problem based on info

assume we assert cache[plr.Name] to dataTemplate which is currently a table var

assume you grab cache[plr.Name] and do changes elsewhere, this will replicate to dataTemplate

the solution is to deeply clone the datatemplate before applying to cache

the reason why this occurs is because datatemplate is a table reference, whenever you assign a var to a table reference, you’re not duplicating the table, you are providing reference to the existing table

2 Likes

Thanks man, I already got this solution from another person with a bit more detail. Thanks for the reinforcement though!! This took a lot to iron out.

1 Like

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