Attempt to call missing method

I have created the following model

local PlayerLeagueUpdateModel = { }
PlayerLeagueUpdateModel.__index = PlayerLeagueUpdateModel

function PlayerLeagueUpdateModel.new(playerKey: StringValue)
	local self = setmetatable({ }, PlayerLeagueUpdateModel)
	self.PlayerKey = playerKey
	return self
end

function PlayerLeagueUpdateModel:GetPlayerKey()
	return self.PlayerKey
end

return PlayerLeagueUpdateModel

In a separate file I create a new instance of this model and I call GetPlayerKey(). Then print the result. It prints the expected value. I have provided the code below.
I then fire a bindable event.

local playerLeagueUpdateModel = PlayerLeagueUpdateModel.new(playerKey)
	
local playerKey = playerLeagueUpdateModel:GetPlayerKey()
print(playerKey)

PlayerBindableEvent:Fire(saveEventType, player, playerLeagueUpdateModel)

I have a script watching for this bindable event. The script is provided below

local function HandlePlayerActiveInLeagueUpdate(eventType: EventTypes, player: Player, playerLeagueUpdateModel: PlayerLeagueUpdateModel)
	local playerKey = playerLeagueUpdateModel:GetPlayerKey()
	print(playerKey)
	
	PlayerDataManager.UpdatePlayerLeagueData(player, playerLeagueUpdateModel)
end

When I call playerLeagueUpdateModel:GetPlayerKey() I get the following error message

attempt to call missing method ‘GetPlayerKey’ of table

What am I doing wrong? I have seen a couple of other people ask similar questions but I’ve not found any solutions.

Table passed through the bindable event loses the metatable. The recipient receives its own copy of the sent table. Look at the memory addresses in the illustration below:

local table1 = {}
setmetatable(table1, {__index = table1})
print(tostring(table1)) --> table: 0xc39230a8de3c0092
print(getmetatable(table1)) --> {...}

local bindableEvent = Instance.new("BindableEvent")

bindableEvent.Event:Once(function(table2)
	print(tostring(table2)) --> table: 0x1d5aef0dfc7c5b72
	print(getmetatable(table2)) --> nil
end)

bindableEvent:Fire(table1)

The question is, why try to send the object through the bindable event. This is a lot of data sent via a signal.

Any script that needs access to the object should simply require it, or access the data from the module that required the object. For example:

local Class = require(Class)

module1.Objects = {}
-- Let's say some third script calls a function in this module that creates
-- an object for the sent player and stores it in module1.Objects.

-- inside the function:
module1.Objects[player] = Class.new()
local module2 = {}
local module1 = require(module1)

-- Call a method of the object returned by the Class:
module1.Objects[somePlayer]:PerformATask()

Bindable events and functions should be used for inter-script communication and signaling more than sending the data, normally in cases where two scripts depend on each other, but do not share functionality.

Thank you for your quick response. That backs up my thoughts (Chat GPT was telling my code was fine).

The question is, why try to send the object through the bindable event.

I have two separate modules (PlayerData and League). I want to save some some of the league data against the Player (e.g. their final position, current position etc). Obviously I can simply use the property to get the value, but I expected the instance to be passed.

No problem, and yeah, ChatGPT is an awesome assistance, but it can make mistakes.

So, two separate modules that have to share some data. I’d say the approach entirely depends on your script architecture. Personally, I like to have a module that “points” to all objects and data regarding clients.

Something like this.
local Client = {}

local playerInfo: {[Player]: any} = {}

local DataManager = require(...)
local CharacterModule = require(...)

function Client.Init(player: Player)
	playerInfo[player] = {}
	playerInfo[player].Data = {}
	DataManager.DoThisAndThat()
	playerInfo[player].Character = CharacterModule.new()
	-- and so on
end

function Client.Discard(player: Player)
	-- save and clear
end

setmetatable(Client, {__index = playerInfo})
return Client

Then any script can require Client and access data via Client[player].Data etc. The table the module returns is actually very lightweight, because all that data and objects reside in the module itself, and are accessible via __index metamethod.

This is just one way of going about it, though it has worked well for me numerous times. You could prefer a different hierarchy of modules. Let’s say you have a some DataController that controls both the data saving and LeagueUpdate. Maybe, in your case, you could have a module that requests the player data, updates the live data, keeps reading and updating the live data while occasionally calling auto save, then wraps everything up, updates and saves the data, and clears the live data at the end.

The general idea is modularity and hierarchy. Modules higher in hierarchy manage modules lower in the system. And two modules cannot require each other. It makes more sense to have a module that depends on the functionality of those two. At some point bindable signals come in handy too, but they are more suitable for lower amounts of sent data and signalling.

1 Like

Thanks for that tip. I feel I have locked myself to an architecture that will be problematic in the future.

Although I am interested in why bindable events are only used for small amounts of data? I assumed as this is being passed around in memory there would be few issues with doing it this way, what are the negatives of passing larger objects around (apart from what you have already explained)?