Need help figuring out why my inventory data sometimes gets reset

(I use ProfileService)

Around once a week, sometimes player’s inventory data is reset, and I can’t figure out why. I am saving a lot of other similar data in the same way, and it has never been reset before. I’ll put each step that my data loading and saving goes through and hopefully someone can point out where it’s going wrong.

Here is where the data is loaded through a server Script via PlayerAdded. profile.Data.Items is a blank table when the player first ever joins the game by default.

local function PlayerAdded(player: Player)
	local hasLoaded = Instance.new("BoolValue")
	hasLoaded.Name = "hasLoaded"
	hasLoaded.Value = false
	hasLoaded.Parent = player
	
	local profile = ProfileStore:LoadProfileAsync("Player_"..player.UserId.."_Save5")
	
	hasLoaded.Value = true

	-- Cut out some unrelated code here
	
	if player:IsDescendantOf(Players) == true then
		Manager.Profiles[player] = profile
		CreateDataFolder(player)
		
		task.wait(3) -- I put a small delay due to weird inventory behavior if it loaded before my other client data. I do this with most of my data that are tables.
		dataEvent:FireClient(player, "loadItems", profile.Data.Items)
	else
		profile:Release()
	end
end

Here is where the client listens to that event and loads the items (within a ModuleScript that is called by a LocalScript after the hasLoaded variable from above is set to true):

local ItemInfo = require(RS.Modules.ItemInfo)
local itemsLoaded = false --My functions that add/remove items return if this is still false.
local items = {}

function BackpackClient.Start()
	dataEvent.OnClientEvent:Connect(function(eventtype, data)
		if eventtype == "loadItems" then
			BackpackClient.LoadItems(data)
		end
	end)
end

function BackpackClient.LoadItems(data)
	for itemName, item: Types.Item in pairs(ItemInfo.Items) do
		for i, loadedItem: Types.Item in pairs(data) do
			if item.ItemId == loadedItem.ItemId then
				item.ItemAmount = loadedItem.ItemAmount -- I load data this way so if I make changes to data held within an item, it will update for existing items.
				table.insert(items, item)
			end
		end
	end
	itemsLoaded = true
	BackpackClient.SetupBackpack()
end

I save the data for Items when the player gets an item and after they use an item.

function BackpackClient.AddItem(itemName)
	if itemsLoaded == false then return end 
	for i, item in pairs(items) do
		local nameForSearch = string.gsub(item.Name, " ", "")
		if nameForSearch == itemName then
			item.ItemAmount = item.ItemAmount + 1
			clientDataEvent:Fire("saveData", "Items", items)
			return
		end
	end
	-- This is for if the item isn't already in the inventory, then it makes an icon for that item, but I cut out that part of the code.
	for i, item: Types.Item in pairs(ItemInfo.Items) do
		if i == itemName then
			item.ItemAmount = item.ItemAmount + 1
			table.insert(items, item)
		end
	end
	clientDataEvent:Fire("saveData", "Items", items)
end

function BackpackClient.RemoveItem(item)
	if itemsLoaded == false then return end
	for i, itemInfo in pairs(items) do
		if itemInfo.Name == item.Name then
			itemInfo.ItemAmount -= 1
			if itemInfo.ItemAmount <= 0 then
				table.remove(items, i)
			end
			clientDataEvent:Fire("saveData", "Items", items)
		end
	end
end

That event sends the info along to the server to be saved here:

function DataManagerServer.SaveData(player: Player, dataType, value)
	local profile = Manager.Profiles[player]
	local profileData = profile.Data
	local playerDataFolderChildren = player.Data:GetChildren()

	if dataType == "Items" then
		profileData.Items = value
	end
	-- I cut out a lot of other data variables

	for i, data in pairs(playerDataFolderChildren) do
		if dataType == "Items" then continue end
        -- I exempt tables from this because you can't store them within the player like you can `NumberValues`, etc.		

		if data.Name == dataType then
			data.Value = value
		end
	end
end

From there, ProfileService handles the saving. I have other tables of info being saved and none of them have ever reset, and I’ve compared them with how I’m saving my Items and can’t tell where the data loss could be occurring. (Sorry if this was a long post I tried to keep it as short as possible)

So, we can rule out data being sent from the client right? Looking at your code if we could somehow save data on the server (just set up a server script). What I feel is happening is you have no if not then return ends. Not always, but sometimes, the client sends invalid data or the profile failed to load. This happens, and in your savedata script I see no protection against that. Infact your whole script runs regardless of if a profile was ever loaded. So the client sends your “clientDataEvent:fire” with what you passed. In all my server scripts I have checks to make sure I have ZERO nils. I can actually see how PS would save nil and it clears out your table. Also this is a unique way (not wrong) to save data. You have 1 function that you call everywhere. I get you want to simplify this but maybe you should seperate your functions that save data. For example I have function in my PS that adjust just currencies, one that handles just pets, one for just quest. They are all independent of each other. First fix the possible nil returns. Because you could 100% be trying to load a profile and adding nil data to the table. Secondly, Modularizing your code is good, but it isn’t a one function solve all, that is actually bad practice IMO. Third, not required, but you should define types this verifies the data you expect on the server from like a config file or something. You actually have some of it in your code already for example: player: Player. But you cannot keep your same function if you define the dataType, as you are trying to do a one function solve all. Here is the top half of my egg function, this also saves the pet hatched from the egg to the player data.

local function Hatch(player: Player, egg: stirng, amount: number)
	local profile = PlayerData.Profiles[player]
	if not profile then return end
	
	local onCooldown = hatchCooldown[player]
	if onCooldown then return end
	
	local eggConfig = EggsConfig.GetConfig(egg)
	if not eggConfig then return end

-- here is an extreme one that I make sure NEVER returns something I do not want
function Quests.UpdateQuestProgress(player: Player, npc: string, config, id: string, amount: number?)
	amount = amount or 1
	local npcName = tostring(npc) -- this is a test for fun you can even do tonumber and get different outcome
	
	local config = config
	if not config then return end
	
	local questItems = config.QuestItem
	if not questItems then return end

	local questItemAmounts = config.QuestItemAmount
	if not questItemAmounts then return end
	
	local profile = PlayerData.Profiles[player]
	if not profile then return end
	
	local playernNpcData = profile.Data.Quests[npcName]
	if not playernNpcData then return end

Hope this helps.

Of course I remember that if you die somehow or oof, it clears the backpack but your code if I am following it right, adds to the backpack from what was saved in the profile.

1 Like

Alright I implemented a tedious amount of if (whatever) == nil then return end checks all throughout my data saving and loading in hopes that I won’t have to add separate functions for all of my data saving/loading.

If that doesn’t work, then I will start to separate everything into more specific saving/loading functions. I’ll come back later to this to mark your reply as the solution if it works, thanks for the advice!

1 Like

No problem, I think what you are doing is fine, but at some point to better trouble shoot you can start spliting the functions, because think about it from my perspective. I come in to your experience to help you code, you have one huge function handle all saving data, you have problem with data well now what? I will monitor the post, feel free to DM me if it still has problems.

1 Like