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())
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:
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:
The Player data Table will see that, for example, the GetPlayer key has a value of nil.
Lua then checks if a metatable exists for the Player data Table, which it does
Since it failed to “index” a key, it looks for the __index metamethod, which it finds is another Table
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.
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.
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…?
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!
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.