Metatable loses profile-service functions

Hi there, I made a new playerClass before implementing the profileservice and I am trying to work around it now. Right now this is the profile class

local module = {}

local template = {
	Name = "";
	leaderstats = {
		Spins = 0;
	};
	Upgrades = {
		IncrementSpeed = 1
	};
	plr = nil;
	GottenUgc = {};
	LuckBoost = 0;
}

module.__index = module

function module.newWithData(plr,data)
	local metaTable = setmetatable(data,module)
	metaTable.Data.Name = plr.Name
	metaTable.Data.plr = plr
	return metaTable
end

--function module.New(plr)
--	local data = setmetatable(template,module)
--	data.Name = plr.Name
--	data.plr = plr
--	return data
--end

function module:MakeLeaderstats(plr)
	local leaderstats = Instance.new("Folder")
	leaderstats.Parent = plr
	leaderstats.Name = "leaderstats"
	
	local spins = Instance.new("IntValue")
	spins.Parent = leaderstats
	spins.Name = "Spins"
	spins.Value = 0
end

function module:UpdateSpins()
	print(self)
	local leaderstats = self.Data.plr.leaderstats
	leaderstats.Spins.Value += 1
	self.Data.leaderstats.Spins += 1
end

function module:CanSpin() : boolean
	print(self)
	if self.Data.leaderstats.Spins - 2 >= 0 then
		print("User has adequate spins")
		return true
	end
	print("User is poor")
	return false
end

function module:HasUgc(id)
	return table.find(self.Data.GottenUgc,id)
end

function module:ClaimedUgc(id)
	print("find index of ugc and destroy it :))")
	local index = table.find(self.GottenUgc,id)
	table.remove(self.Data.GottenUgc,index)
end

function module:GotUGC(id)
	table.insert(self.Data.GottenUgc,id)
end

function module:SpendSpins()
	if self.Data.leaderstats.Spins - 2 >= 0 then
		self.Data.leaderstats.Spins -= 2
		self.Data.plr.leaderstats.Spins.Value = self.Data.leaderstats.Spins
	end
end

function module:GiveCustomSpins(number)
	self.Data.leaderstats.Spins += number
	self.Data.plr.leaderstats.Spins.Value += number
end

function module:GetSpins()
	return self.Data.leaderstats.Spins
end

function module:giveSpin()
	local spins = self:GetSpins()
	spins += 1;
	self:UpdateSpins()
end

return module

The thing is now that when I try to do profile:Release after I fetch it in another module that holds all of these profiles, I get an error

local playerClass = require(script.playerClass)
local profileService = require(script.ProfileService)

local plrs = game:GetService("Players")

local dataStoreKey = "someKey"

local template = {
	Name = "";
	leaderstats = {
		Spins = 0;
	};
	Upgrades = {
		IncrementSpeed = 1
	};
	plr = nil;
	GottenUgc = {};
	LuckBoost = 0;
}

local playerStore = profileService.GetProfileStore(
	dataStoreKey, template
)

local players = {
	-- [PlayerName] = {} this is the template
}

local profiles = {
	
}

function players.makePlot(plr)
	local unclaimed = workspace.Unclaimed:GetChildren()

	local playersPlot = unclaimed[1]
	unclaimed[1].Name = plr.Name
	unclaimed[1].Parent = workspace.Claimed
	return playersPlot
end

function players.addPlayer(plr : Player)
	local profile = playerStore:LoadProfileAsync("Player_"..plr.UserId)
	local existingPlrData
	if profile ~= nil then
		profile:AddUserId(plr.UserId)
		profile:Reconcile()
		profile:ListenToRelease(function()
			players[plr] = nil
			profiles[plr] = nil
			
			plr:Kick()
		end)
		if plr:IsDescendantOf(plrs) == true then
			existingPlrData = playerClass.newWithData(plr,profile)
			players[plr.Name] = existingPlrData
			profiles[plr] = profile
		else
			profile:Release()
		end
	else
		plr:Kick()
	end
	existingPlrData:MakeLeaderstats(plr)
	return existingPlrData
end

function players.fetchData(plr)
	local plrData = players[plr.Name]
	return plrData
end

function players.fetchProfile(plr)
	return profiles[plr]
end

game.Players.PlayerRemoving:Connect(function(plr : Player)
	local profile = profiles[plr]
	print(profile)
	if profile ~= nil then
		profile:Release()
	end
end)


return players

The error is:

What am I doing wrong? I know it has something to do with how I initialize my metatables but I don’t know how to fix it.

3 Likes

Where’s the :Release() function? I see calls of it, but no definition.

1 Like

The release function is in profileservice, i require it in the second script in the top and load a profile using LoadProfileAsync which provides the :Release() function

1 Like

Are you able to send the code for the Release function?

2 Likes

2 Likes

Do you use table.clear, or set all table keys to nil during or after execution?

It seems the profile isn’t being properly cleaned up (as in removed from cache, at least that’s what I’m assuming) when released.

Also ensure your metatable is being set up properly, as that’s also a pretty good way to get this exact error.

Double also, make sure you’re using table.clone when creating new data tables, as using the same one will cause the original reference to it to get cleaned up, cleaning up future references.

1 Like

Do you use table.clear, or set all table keys to nil during or after execution?

The only clearing I am doing is here

But this is how I should do it as per the documentation.

Also ensure your metatable is being set up properly, as that’s also a pretty good way to get this exact error.

It used to work, but what I am guessing is that now the profile-service functions are being cleared up and replaced, not sure thought.

Double also, make sure you’re using table.clone when creating new data tables, as using the same one will cause the original reference to it to get cleaned up, cleaning up future references.

What do you mean by this?

2 Likes

The function,

function module.newWithData(plr,data)
	local metaTable = setmetatable(data,module)
	metaTable.Data.Name = plr.Name
	metaTable.Data.plr = plr
	return metaTable
end

uses the data variable to set up a new metatable. What does this data table reference to, and is the same exact value reused every time this function is called? In the script you provided, I can see where it’s called, but not enough information is given to see what the value is exactly.

2 Likes

Ahh, that’s what you mean. The data variable is the profile that was just loaded

In the second script I sent in the original post I had this block

if plr:IsDescendantOf(plrs) == true then
			existingPlrData = playerClass.newWithData(plr,profile)
			players[plr.Name] = existingPlrData
			profiles[plr] = profile
		else

The profile (which is then named data in the function) is this:

local profile = playerStore:LoadProfileAsync("Player_"..plr.UserId)

So I just set a metatable of the profile with all the functions with the module to have all those functions too since they are pretty important, the functions of the module work but it seems that when the user is removed from the server, the profileServices functions are lost

Edit: ProfileService uses the template I provide it to create the user data and then in the documentation it adds the profile to a table, but I want the profile to also have the functions of the playerClass so I make a new class with playerClass.New(plr,profile) and make a metatable. All works well until playerRemoving which then, profile, loses all the functions (or at least :Release() and :IsActive()

2 Likes

Try using table.clone and see what that yields you.

function module.newWithData(plr,data)
	local metaTable = setmetatable(table.clone(data),module)
	metaTable.Data.Name = plr.Name
	metaTable.Data.plr = plr
	return metaTable
end

You can also use this DeepCopy function, as table.clone is just a shallow copy, for a more complete table copy with isolated sub-tables too.

local function DeepCopy(t: {any})
	local Tables = {}
	local function Main(t: {any})
		local Copy
		if (typeof(t) == "table") then
			local Value = Tables[t]
			if (Value ~= nil) then
				--warn(`[Table] Potential C stack overflow! ({tostring(t)})`)
				Copy = Value
			else
				Copy = {}
				Tables[t] = Copy
				for i, v in next, t do
					Copy[Main(i)] = Main(v)
				end
			end
		elseif (typeof(t) == "userdata") then
			Copy = newproxy(true)
			local Metatable = getmetatable(t)
			if (typeof(Metatable) == "table") then
				setmetatable(Copy, Metatable)
			else
				warn(`[Table] Issue copying userdata: metatable.__metatable is set!`)
			end
		else
			Copy = t
		end
		return Copy
	end
	
	local Result = Main(t)
	table.clear(Tables)
	Tables = nil
	return Result
end
2 Likes

Doing the table.clone() almost crashed studio and I got these errors:

DataStoreService: CantStoreValue: Cannot store Dictionary in data store. Data stores can only accept valid UTF-8 characters. API: UpdateAsync, Data Store: _playerStore_  -  Studio

[ProfileService]: DataStore API error [Store:"_playerStore_";Key:"Player_134551117"] - "104: Cannot store Dictionary in data store. Data stores can only accept valid UTF-8 characters." 
[ProfileService]: Entered critical state 
2 Likes

Extremely odd.

I’m going to need more information (regarding ProfileService) in order to help you. Without the complete method run-down behind the :Release() function, I cannot help you.

There are function calls within the Release function. The IsActive function can be ignored, but I need a copy of the SaveProfileAsync function.

2 Likes

The issue is that the previous metatable of profile (data param) is overwritten with module.

local t = setmetatable({}, {__index = function(self, k) return "original" end})
print(t.key) --> original
setmetatable(t, {})
print(t.key) --> nil

Personally I rather keep the metatable and have profile be a member of the object instead of being the object itself, but you can also take the other paths.

If the metatable of profile is only class Profile, you could set playerClass to inherit from Profile through a chain of two metatables.

module = {}
module.__index = module
setmetatable(module, Profile)
2 Likes

But I want the player class to get the player’s data, not the whole ProfileService. in the profile, it also contains the user’s data, ProfileService doesn’t.

Personally I rather keep the metatable and have profile be a member of the object instead of being the object itself, but you can also take the other paths.

What do you mean with this?

1 Like

I just realized also that it isn’t even the problem of the metatable. I am keeping a separate folder with the profiles, because I thought it would be good idea to keep those separate. So I am just putting the profile in the profiles table. So it shouldn’t get overridden by anything.

2 Likes

You can read the API here : API - ProfileService

1 Like

Ohhh you’re using the ProfileService from Mad Studio, that makes so much more sense :man_facepalming: my bad

The segment in the second script provided,

		if plr:IsDescendantOf(plrs) == true then
			existingPlrData = playerClass.newWithData(plr,profile)
			players[plr.Name] = existingPlrData
			profiles[plr] = profile
		else
			profile:Release()
		end

is conflicting with the PlayerRemoving connection at the bottom,

game.Players.PlayerRemoving:Connect(function(plr : Player)
	local profile = profiles[plr]
	print(profile)
	if profile ~= nil then
		profile:Release()
	end
end)

I’m pretty sure the ListenToRelease uses BindableEvents, which by default assumes Deferred thread behavior, aka task.defer.
It’s best to just clean the variables on the listener and when it’s called (and should take immediate action), so…

		if plr:IsDescendantOf(plrs) == true then
			existingPlrData = playerClass.newWithData(plr,profile)
			players[plr.Name] = existingPlrData
			profiles[plr] = profile
		else
			players[plr] = nil
			profiles[plr] = nil
			profile:Release()
		end

You can also set Workspace.SignalBehavior to Immediate to eliminate all root causes of this particular issue for all instances of :Release().

2 Likes

Ohhh you’re using the ProfileService from Mad Studio, that makes so much more sense :man_facepalming: my bad

Haha, yeah sorry for not clarifying.

I’m pretty sure the ListenToRelease uses BindableEvents, which by default assumes Deferred thread behavior, aka task.defer .
It’s best to just clean the variables on the listener and when it’s called (and should take immediate action), so…

This fixed my :Release method error not existing, but now I am having an issue with IsActive()

ServerScriptService.playerModule.ProfileService:2399: attempt to call missing method 'IsActive' of table 
2 Likes

Try this and see if your issue persists.

1 Like

Still having this problem unfortunately.

2 Likes