ProfileService with metatables bug - setting every in server's data to the same

Okay, so this is the line that sets data:

self = setmetatable(Data, {__index = profile});
Data.Profiles[player.UserId] = self;

For whatever reason, everyone in the server gets set to the data of the last person that joined:
image

Please provide more context. There isn’t really much to go off from those two lines…

Entire function:

function Data.PlayerJoined(player)
	warn("LOADING")
	local profile = DataStore:LoadProfileAsync(tostring(player.UserId), false, "Repeat");
	local self = {};
	
	if profile and player then
		warn("Loading 1/4")
		if player.Parent then
			warn("Loading 2/4")
			profile:AddUserId(player.UserId);
			profile:Reconcile();
			
			profile:ListenToRelease(function()
				warn(Data.Profiles[player.UserId])
				Data.Profiles[player.UserId] = nil;
				Data.Rake[player.UserId] = nil;
				player:Kick("Your data is released.");
			end)
			
			if Data.Profiles[player.UserId] then
				Data.Profiles[player.UserId]:Release();
				player:Kick("Your data is currently being locked in session: " .. game.JobId .. ", please rejoin.");
			else
				warn(profile)
				warn("Loading 3/4")
				self = setmetatable(Data, {__index = profile});
				Data.Profiles[player.UserId] = self;
				warn("Loaded 4/4");
				return (self);
			end
		end
	end
	return self;
end

Perhaps the ‘profile’ variable is the same for every player?
You could try printing it out to be sure.

What do you mean? Profile is different. The issue is when a new player joins, every player in the servers get set to the new player’s data.

Yes, if there’s an issue with your profile loading, that could mean every player’s data is set to the same thing.
I.e. if ‘Datastore:LoadProfileasync()’ returns the same value.

Are you sure that returns what you expect it to? You can just add a print(profile) to check it’s value.

I’ve added those, it’s different per player, but I think you’re misunderstanding this.

Player 1 is in the server with 50 dollars
Player 2 joins with 1000 dollars

Now, player 1 has 1,000 dollars, and so does player 2. That has nothing to do with LoadProfileAsync, as it’s taking old data away, and already been called.

1 Like

Would like to bump this, it is a serious issue in my game.

If it’s not that, then it’s probably something with your OOP structure. I’m not all too familiar with it in lua, but usually the entire context is required to fix difficult bugs like this in OOP.

I hope someone can help you :+1:.

So hi it’s me again and I figured smth like this would pop up when I gave u the solution before. So what I overlooked in my previous solution was when you reference a table, it doesn’t return a copy of the table but a memory address which points to it. So with this in mind, you are currently setting a the profile of the player as the metatable to Data and returning it. However here you are returning the Data table with a metatable attached not a profile table with a metatable attached. So whenever a new player joins the Data’s metatable is overwritten causing all existing player data with its metatable being overwritten to the new player’s data, because they all are referencing a common table.

So, in this case, would i need a deepcopy table to patch that?

Edit: Tried that and it wouldn’t load. 5k errors in console

Okay - so I did a shallow copy and this worked. HOWEVER, we’re now back to square one, where :Release() doesn’t work.

Bump. Big issue in my game, it’s unplayable. :sad:

Okay I’m onto something here, I added a warin in :Release(), and it’s not seeing the profile, but seeing the entire data module instead.

I FIXED! The fix:

function Profile:Release(userId)
	warn("Releasing Data!")
	local tempSelf = self.__index._active_profile_stores[1]._loaded_profiles[tostring(userId)]
	if tempSelf._view_mode == true then
		warn("Profile was in view mode!")
		return
	end
	if tempSelf:IsActive() == true then
		warn("Profile is actived, and being released!")
		task.spawn(SaveProfileAsync, tempSelf, true)
	end
end

Inside of profile service.

When you start to edit dependencies, it’s usually not for the best as issues may pop up later in the future. And your methods will only work for this particular function (Release) but not for others. Another relatively simple fix is this.

setmetatable(profile, {__index = function(self, index)
    if ProfileService[index] then
        return ProfileService[index]
    elseif Data[index] then
        return Data[index]
    else
        error(`No member named {index}`)
    end
end}

OR (shortened)

setmetatable(profile, {__index = function(self, index)
    return ProfileService[index] or return Data[index] or error(`No member named {index}`)
end}

The above will work for all methods and not just release. What it does is if a member is not found in the default profile table, it will first look in the ProfileService table and if it isn’t in there then the search will be done on the Data table and neither of em have a that specific member, it will error with the member name for easier debugging.

Oh! You’re amazing for this. I’ll revert my profile service changes and implement this!

Actually now I get these errors:
ServerScriptService.Modules.RakeData:76: No member named Release - Server - RakeData:76
14:34:22.049 Stack Begin - Studio
14:34:22.049 Script ‘ServerScriptService.Modules.RakeData’, Line 76 - function __index - Studio - RakeData:76
14:34:22.049 Script ‘ServerScriptService.Scripts.Server’, Line 42 - Studio - Server:42
14:34:22.049 Stack End - Studio
14:34:22.049 ServerScriptService.Modules.RakeData:76: No member named IsActive - Server - RakeData:76

Adjusted it to this to avoid the error:

				self = setmetatable(profile, {__index = function(a, index)
					warn(a, index)
					if table.find(avoid, index) then
						return ProfileService[index] or Data[index];
					else
						return rawget(ProfileService._active_profile_stores[1]._loaded_profiles[tostring(player.UserId)], index);
					end
				end})

Still errors with release not being apart of table