Setmetatable() not working?

I have defined the following table, which serves as a “class”:

local PlayerSession = {}
PlayerSession.__index = PlayerSession

Also, there’s this function that instances a new PlayerSession object:

function PlayerSession.new(player)
	local session = {}
	...
	return setmetatable(session, PlayerSession)
end

The expected result from the function above is a table with metamethods inherited from the PlayerSession class. But that doesn’t happen. What am I doing wrong?

There doesn’t seem to be any issue with it. I tried this and it worked fine

local PlayerSession = {}
PlayerSession.__index = PlayerSession

function PlayerSession.new(player)
	local session = {}
	session.Player = player

	return setmetatable(session, PlayerSession)
end

function PlayerSession:GetPlayer()
    return self.Player
end

local session = PlayerSession.new(game.Players.LocalPlayer)
print(session:GetPlayer())
1 Like

Yeah, right! But when I try calling a class method it says it’s nil! Here’s the full code, in case someone feels like taking a look:

local PlayerSession do
	PlayerSession = {}
	
	PlayerSession.__index = PlayerSession
	
	function PlayerSession.new(player)
		local session = {}

		session.__Player = player
		session.__UpdateQueue = {}
		session.__UpdateOccupied = {}

		local attempts = 0

		while attempts < SETTINGS.MAX_RETRIES do
			local success, result = pcall(function()
				return DataStore:GetAsync(player.UserId)
			end)

			if success then
				if result then
					for key, value in pairs(SETTINGS.DEFAULT_DATA) do
						result[key] = result[key] or value
					end
				else
					result = SETTINGS.DEFAULT_DATA
				end

				session.__Data = result
				break
			end

			attempts += 1
		end

		if attempts == SETTINGS.MAX_RETRIES then
			return nil
		end

		session.__UpdatedEvents = {}

		for key, _ in pairs(session.__Data) do
			session.__UpdatedEvents[key] = Instance.new("BindableEvent")
		end
		
		return setmetatable(session, PlayerSession)
	end

	function PlayerSession:GetPlayer()
		return self.__Player
	end

	function PlayerSession:Get(key)
		return self.__Data[key] or error('Non-existant key "'..key..'"!')
	end

	function PlayerSession:GetUpdated(key)
		return self.__UpdatedEvents[key] and self.__UpdatedEvents[key].Event or error('Non-existant key "'..key..'"!')
	end

	function PlayerSession:GetAndUpdated(key)
		if not self.__Data[key] then return error('Non-existant key "'..key..'"!') end

		return self:Get(key), self:GetUpdate(key) 
	end

	function PlayerSession:Update(key, callback)
		if not self.__Data[key] then return error('Non-existant key "'..key..'"!') end

		if self.__UpdateOccupied[key] then
			self.__UpdateQueue[key] = self.__UpdateQueue[key] or {}
			table.insert(self.__UpdateQueue[key], callback)
		else
			self.__UpdateOccupied[key] = true
			self.__Data[key] = callback(key)
			self.__UpdatedEvents[key]:Fire(self.__Data[key])

			while self.__UpdateQueue and #self.__UpdateQueue > 0 do
				self.__Data[key] = table.remove(self.__UpdateQueue, 1)(key)
				self.__UpdatedEvents[key]:Fire(self.__Data[key])
			end

			self.__UpdateOccupied[key] = false
		end
	end

	function PlayerSession:Set(key, value)
		if not self.__Data[key] then return error('Non-existant key "'..key..'"!') end

		self.__UpdateQueue[key] = {}

		while self.__UpdateOccupied[key] do
			wait()
		end

		self.__Data[key] = value
		self.__UpdatedEvents[key]:Fire(self.__Data[key])
	end
end

When trying to print a new session, this is what pops out:

 {
       ["__Data"] =  ▶ {...},
       ["__Player"] = RicoFox,
       ["__UpdateOccupied"] = {},
       ["__UpdateQueue"] = {},
       ["__UpdatedEvents"] =  ▶ {...}
}

You’re misunderstanding how the __index metamethod works.

printing the Table you receive via PlayerSession.new will only print the data directly contained by that Table, exactly as you saw. __index gets invoked when you try to access a member of this returned Table, “falling back” to the PlayerSession Table, as that is what you have set __index to.

If you attempt to call one of the methods you’ve written, assuming you don’t make a typo, it should work as you expect:

  1. The Player data Table will see that, for example, the GetPlayer key has a value of nil.
  2. Lua then checks if a metatable exists for the Player data Table, which it does
  3. Since it failed to “index” a key, it looks for the __index metamethod, which it finds is another Table
  4. It attempts to “index” the key in this __index Table, possible starting this process over if that still returns nil

In your code, it stops at (4) as PlayerSession itself has no metatable. Also worth noting is that you can make __index a function with the arguments of the Table being accessed (as a given metatable can be applied to multiple Tables) and the key that was nil in said Table.

E.g.:

PlayerSession.__index = function(t, i)
    return PlayerSession[i]
end

Is essentially what PlayerSession.__index = PlayerSession does, just less efficiently.

1 Like

Oh, I was wrong to think that the print function considered associated metatables. Thanks for your explanation!

Although, when trying to call the :GetPlayer method from an instanced PlayerSession object, it returns:
ServerScriptService.Server:6: attempt to call a nil value

If you try to print that same object, that output I mentioned previously is what pops out, meaning that it exists and is well defined.

Sorry for a lack of response, fell asleep early!

I just tested your code on Studio and the method calls all work fine for me?

What is your broken “Server” code that’s using PlayerSession?

1 Like

That’s the one:

PlayerDataService.SessionAdded:Connect(function(Session)
	print(Session:GetPlayer())
end)

Which is being sent by:

if session then
	PlayerDataService.Private.Sessions[player] = session
	PlayerDataService.Private.SessionAddedEvent:Fire(session)
else
	PlayerDataService.Private.SessionFailedEvent:Fire(player)
end

It seems like it loses the associated metatable when being parsed by a BindableEvent…?

1 Like

That appears to be the case! BindableEvents do a deep copy of Tables they are passed, without re-applying metatables.

local t = {{}}

workspace.Event.Event:Connect(function(t2)
	print(t == t2)       -- false
	print(t[1] == t2[1]) -- false
end)

print(t)
workspace.Event:Fire(t)

I’m not sure why you’re being so indirect, though. Just make PlayerSession a ModuleScript instead of using BindableEvents.

I did it so it would act like PlayerAdded does for players, so it would be possible to access the sessions as soon as they’re available. I thought, of course, that BindableEvents parsed tables by reference! xP

Well, I’m going to revise my architecture based on everything you said. Thank you for the help! :slight_smile:

You probably don’t need to revise your architecture just because of this, just adopt a custom signal class and substitute all uses of BindableEvents for the signal class. The issue of metatables is part of why developers may choose to create a custom signal class.

There’s already a bunch readily available if you do a quick search through existing resources: for example, the Signal class found in NevermoreEngine. You can just copy the source of this module into your own ModuleScript and you’re good to go.

1 Like

Ooo, just what I needed! Thanks a lot! :slight_smile: