Weird metatable issue

I have a class with a constructor, and everything looks completely normal, but whenever I try to call a method of an object constructed, it errors “attempt to call missing method of table.” I have no idea why it’s doing this, as the code looks completely normal. I switched it out for . instead of :, and it worked fine.
I’ve done things with class methods in the past, so I have no idea why it’s suddenly not working. I was about to not make this post, but Trove, which I have not modified at all, is erroring, saying that Destroy is also a missing method. I’m literally following exact syntax, and I looked in the source code to make sure I was doing it right. I’ll only send small snippets since the Class is a bit long, but I’ll show you what I mean with all of this

function Block.new(...) -- This is not the whole script but it's enough for clarity purposes
    local self = Block.mark(...)

    self._trove = Trove.new()

    return self
end

function Block.mark(...) -- I know this organization looks weird but it makes more sense in the full code.
    local self = setmetatable({}, Block)
    return self
end

function Block.Destroy(block) -- originally was :Destroy without the parameters but gave me the error I was talking about
    if not block._trove then return end
    block._trove:Destroy() -- attempt to call missing method 'Destroy' of table
end

I know this is weird and convoluted but I appreciate all help! Thank you!!

4 Likes

local Block = {}
Block.__index = Block

function Block.new(pig, cow)
local self = setmetatable({
Pig = pig
Cow = cow

}, Block)

return self
end

function Block:whatsound()
print(self.pig, self.cow) — oink and moo
end

return Block

(Wrote this on my phone, sorry about the formatting).

3 Likes

Yeah, this is basically what I’m doing, which is why I’m so confused on why methods aren’t working since this makes sense
Edit: I just realized I don’t show the Block and __index part, but they’re there

The way you are doing it doesn’t quite make sense, I am not sure why because I just woke up and on my phone, but if you do it my way, it should fix the issue.

I tried doing it your way, that was what was erroring before I made the post and what I’m trying to fix with Trove as well

15:51:52.741 ServerScriptService.Script:405: attempt to call missing method ‘Destroy’ of table - Server - Script:405

The most common error I see is that people mispell the __index metamethod which is how you get the error. Similar to this other forum member:

Otherwise

Reprocode in one serverscript to make thigns easy to replicate and run:

Summary
-- Trove
-- Stephen Leitnick
-- October 16, 2021

local FN_MARKER = newproxy()
local THREAD_MARKER = newproxy()

local RunService = game:GetService("RunService")

local function GetObjectCleanupFunction(object, cleanupMethod)
	local t = typeof(object)
	if t == "function" then
		return FN_MARKER
	elseif t == "thread" then
		return THREAD_MARKER
	end
	if cleanupMethod then
		return cleanupMethod
	end
	if t == "Instance" then
		return "Destroy"
	elseif t == "RBXScriptConnection" then
		return "Disconnect"
	elseif t == "table" then
		if typeof(object.Destroy) == "function" then
			return "Destroy"
		elseif typeof(object.Disconnect) == "function" then
			return "Disconnect"
		end
	end
	error("Failed to get cleanup function for object " .. t .. ": " .. tostring(object), 3)
end

local function AssertPromiseLike(object)
	if
		type(object) ~= "table"
		or type(object.getStatus) ~= "function"
		or type(object.finally) ~= "function"
		or type(object.cancel) ~= "function"
	then
		error("Did not receive a Promise as an argument", 3)
	end
end

--[=[
	@class Trove
	A Trove is helpful for tracking any sort of object during
	runtime that needs to get cleaned up at some point.
]=]
local Trove = {}
Trove.__index = Trove

--[=[
	@return Trove
	Constructs a Trove object.
]=]
function Trove.new()
	local self = setmetatable({}, Trove)
	self._objects = {}
	self._cleaning = false
	return self
end

--[=[
	@return Trove
	Creates and adds another trove to itself. This is just shorthand
	for `trove:Construct(Trove)`. This is useful for contexts where
	the trove object is present, but the class itself isn't.

	:::note
	This does _not_ clone the trove. In other words, the objects in the
	trove are not given to the new constructed trove. This is simply to
	construct a new Trove and add it as an object to track.
	:::

	```lua
	local trove = Trove.new()
	local subTrove = trove:Extend()

	trove:Clean() -- Cleans up the subTrove too
	```
]=]
function Trove:Extend()
	if self._cleaning then
		error("Cannot call trove:Extend() while cleaning", 2)
	end
	return self:Construct(Trove)
end

--[=[
	Clones the given instance and adds it to the trove. Shorthand for
	`trove:Add(instance:Clone())`.
]=]
function Trove:Clone(instance: Instance): Instance
	if self._cleaning then
		error("Cannot call trove:Clone() while cleaning", 2)
	end
	return self:Add(instance:Clone())
end

--[=[
	@param class table | (...any) -> any
	@param ... any
	@return any
	Constructs a new object from either the
	table or function given.

	If a table is given, the table's `new`
	function will be called with the given
	arguments.

	If a function is given, the function will
	be called with the given arguments.
	
	The result from either of the two options
	will be added to the trove.

	This is shorthand for `trove:Add(SomeClass.new(...))`
	and `trove:Add(SomeFunction(...))`.

	```lua
	local Signal = require(somewhere.Signal)

	-- All of these are identical:
	local s = trove:Construct(Signal)
	local s = trove:Construct(Signal.new)
	local s = trove:Construct(function() return Signal.new() end)
	local s = trove:Add(Signal.new())

	-- Even Roblox instances can be created:
	local part = trove:Construct(Instance, "Part")
	```
]=]
function Trove:Construct(class, ...)
	if self._cleaning then
		error("Cannot call trove:Construct() while cleaning", 2)
	end
	local object = nil
	local t = type(class)
	if t == "table" then
		object = class.new(...)
	elseif t == "function" then
		object = class(...)
	end
	return self:Add(object)
end

--[=[
	@param signal RBXScriptSignal
	@param fn (...: any) -> ()
	@return RBXScriptConnection
	Connects the function to the signal, adds the connection
	to the trove, and then returns the connection.

	This is shorthand for `trove:Add(signal:Connect(fn))`.

	```lua
	trove:Connect(workspace.ChildAdded, function(instance)
		print(instance.Name .. " added to workspace")
	end)
	```
]=]
function Trove:Connect(signal, fn)
	if self._cleaning then
		error("Cannot call trove:Connect() while cleaning", 2)
	end
	return self:Add(signal:Connect(fn))
end

--[=[
	@param name string
	@param priority number
	@param fn (dt: number) -> ()
	Calls `RunService:BindToRenderStep` and registers a function in the
	trove that will call `RunService:UnbindFromRenderStep` on cleanup.

	```lua
	trove:BindToRenderStep("Test", Enum.RenderPriority.Last.Value, function(dt)
		-- Do something
	end)
	```
]=]
function Trove:BindToRenderStep(name: string, priority: number, fn: (dt: number) -> ())
	if self._cleaning then
		error("Cannot call trove:BindToRenderStep() while cleaning", 2)
	end
	RunService:BindToRenderStep(name, priority, fn)
	self:Add(function()
		RunService:UnbindFromRenderStep(name)
	end)
end

--[=[
	@param promise Promise
	@return Promise
	Gives the promise to the trove, which will cancel the promise if the trove is cleaned up or if the promise
	is removed. The exact promise is returned, thus allowing chaining.

	```lua
	trove:AddPromise(doSomethingThatReturnsAPromise())
		:andThen(function()
			print("Done")
		end)
	-- Will cancel the above promise (assuming it didn't resolve immediately)
	trove:Clean()

	local p = trove:AddPromise(doSomethingThatReturnsAPromise())
	-- Will also cancel the promise
	trove:Remove(p)
	```

	:::caution Promise v4 Only
	This is only compatible with the [roblox-lua-promise](https://eryn.io/roblox-lua-promise/) library, version 4.
	:::
]=]
function Trove:AddPromise(promise)
	if self._cleaning then
		error("Cannot call trove:AddPromise() while cleaning", 2)
	end
	AssertPromiseLike(promise)
	if promise:getStatus() == "Started" then
		promise:finally(function()
			if self._cleaning then
				return
			end
			self:_findAndRemoveFromObjects(promise, false)
		end)
		self:Add(promise, "cancel")
	end
	return promise
end

--[=[
	@param object any -- Object to track
	@param cleanupMethod string? -- Optional cleanup name override
	@return object: any
	Adds an object to the trove. Once the trove is cleaned or
	destroyed, the object will also be cleaned up.

	The following types are accepted (e.g. `typeof(object)`):

	| Type | Cleanup |
	| ---- | ------- |
	| `Instance` | `object:Destroy()` |
	| `RBXScriptConnection` | `object:Disconnect()` |
	| `function` | `object()` |
	| `thread` | `coroutine.close(object)` |
	| `table` | `object:Destroy()` _or_ `object:Disconnect()` |
	| `table` with `cleanupMethod` | `object:<cleanupMethod>()` |

	Returns the object added.

	```lua
	-- Add a part to the trove, then destroy the trove,
	-- which will also destroy the part:
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Destroy()

	-- Add a function to the trove:
	trove:Add(function()
		print("Cleanup!")
	end)
	trove:Destroy()

	-- Standard cleanup from table:
	local tbl = {}
	function tbl:Destroy()
		print("Cleanup")
	end
	trove:Add(tbl)

	-- Custom cleanup from table:
	local tbl = {}
	function tbl:DoSomething()
		print("Do something on cleanup")
	end
	trove:Add(tbl, "DoSomething")
	```
]=]
function Trove:Add(object: any, cleanupMethod: string?): any
	if self._cleaning then
		error("Cannot call trove:Add() while cleaning", 2)
	end
	local cleanup = GetObjectCleanupFunction(object, cleanupMethod)
	table.insert(self._objects, { object, cleanup })
	return object
end

--[=[
	@param object any -- Object to remove
	Removes the object from the Trove and cleans it up.

	```lua
	local part = Instance.new("Part")
	trove:Add(part)
	trove:Remove(part)
	```
]=]
function Trove:Remove(object: any): boolean
	if self._cleaning then
		error("Cannot call trove:Remove() while cleaning", 2)
	end
	return self:_findAndRemoveFromObjects(object, true)
end

--[=[
	Cleans up all objects in the trove. This is
	similar to calling `Remove` on each object
	within the trove. The ordering of the objects
	removed is _not_ guaranteed.
]=]
function Trove:Clean()
	if self._cleaning then
		return
	end
	self._cleaning = true
	for _, obj in self._objects do
		self:_cleanupObject(obj[1], obj[2])
	end
	table.clear(self._objects)
	self._cleaning = false
end

function Trove:_findAndRemoveFromObjects(object: any, cleanup: boolean): boolean
	local objects = self._objects
	for i, obj in ipairs(objects) do
		if obj[1] == object then
			local n = #objects
			objects[i] = objects[n]
			objects[n] = nil
			if cleanup then
				self:_cleanupObject(obj[1], obj[2])
			end
			return true
		end
	end
	return false
end

function Trove:_cleanupObject(object, cleanupMethod)
	if cleanupMethod == FN_MARKER then
		object()
	elseif cleanupMethod == THREAD_MARKER then
		coroutine.close(object)
	else
		object[cleanupMethod](object)
	end
end

--[=[
	@param instance Instance
	@return RBXScriptConnection
	Attaches the trove to a Roblox instance. Once this
	instance is removed from the game (parent or ancestor's
	parent set to `nil`), the trove will automatically
	clean up.

	:::caution
	Will throw an error if `instance` is not a descendant
	of the game hierarchy.
	:::
]=]
function Trove:AttachToInstance(instance: Instance)
	if self._cleaning then
		error("Cannot call trove:AttachToInstance() while cleaning", 2)
	elseif not instance:IsDescendantOf(game) then
		error("Instance is not a descendant of the game hierarchy", 2)
	end
	return self:Connect(instance.Destroying, function()
		self:Destroy()
	end)
end

--[=[
	Alias for `trove:Clean()`.
]=]
function Trove:Destroy()
	self:Clean()
end

local Block = {}
Block.__index = Block

function Block.new(...) -- This is not the whole script but it's enough for clarity purposes
	local self = Block.mark(...)

	self._trove = Trove.new()

	return self
end

function Block.mark(...) -- I know this organization looks weird but it makes more sense in the full code.
	local self = setmetatable({}, Block)
	return self
end

function Block.Destroy(block) -- originally was :Destroy without the parameters but gave me the error I was talking about
	if not block._trove then return end
	block._trove:Destroy() -- attempt to call missing method 'Destroy' of table
end

local testBlock = Block.new(1,"Hello")

testBlock:Destroy()
print("Success")

Sorry for the wait, just got to my computer.

-- In a module script.
local Trove = require(game:GetService("ReplicatedStorage").Packages.Trove)

local Block = {}
Block.__index = Block

function Block.new()
	local self = setmetatable({
		_trove = Trove.new()
		
	}, Block)
	
	return self
end

function Block:mark()
	return self._trove:Construct(Instance, "Part")
end

function Block:Clean()
	self._trove:Destroy()
end

return Block
-- In a server script.
local Block = require(script.ModuleScript)
local CreateBlock = Block.new()
local Part = CreateBlock:mark()
Part.Parent = workspace

task.wait(5)

CreateBlock:Clean()
2 Likes

I have it spelled __index

What do you mean by this? Sorry XD

2 Likes

This is almost what I’m doing. In the full script, mark creates a marker for a Block, and new takes a marker and turns it into a physical block. I’m just still confused on why methods of Trove and the class aren’t working.

1 Like

The script I created works for me, did you test it out in your place yet? It might be caused by how you are requiring it and making the mark.

1 Like

I can’t really test it in the same game since I’d be removing half the code

Here are the full functions if that helps:

function Block.new(...)
    local self = Block.mark(...)

    self._trove = Trove.new()
    self.Part = self._trove:Add(self.Type.Mesh:Clone())
    self.Part.Parent = workspace.World
    self.Part.CFrame = self.CFrame
    self.Part.Color = self.Color
    
    if self.Texture then
        for _, face in Enum.NormalId do
            local newTexture = Instance.new("Texture")
            newTexture.Parent = self.Part
            newTexture.Face = face
        end
    end

    self.Part.Material = self.Material
    self.Part.Transparency = self.Transparency

    self.Part.Changed:Connect(function(property)
        if self[property] then
            Blocks.ChangeBlock:Fire(self, property, self[property])
        end
    end)
    CollectionService:AddTag(self.Part, "Block")

    return self
end

function Block.mark(...)
    repeat task.wait() until Loaded
    local args = ({...})[1]
    local self = setmetatable({}, Block)
    
    self.Type = GetBlocksList().Cube
    self.CFrame = CFrame.new()
    self.Color = Color3.new(1, 1, 1)
    self.Texture = nil
    self.Material = Enum.Material.SmoothPlastic
  
    self.Transparency = 0
    for arg, val in args do
        if self[arg] then
            self[arg] = val
        else
            if arg ~= "Name" then
                warn(arg .. " is not a property of Block") 
            end
        end
    end
    self.Name = self.Type.Mesh.Name

    return self
end
2 Likes

Can I see how you are requiring this? It is not a Trove issue or module issue from the looks of it.

1 Like
local Trove = require(ReplicatedStorage.Packages.Trove)
1 Like

Sorry for the confusion, I meant where you call Block.mark()

1 Like

Trove isn’t used in Block.mark, am I misunderstanding?

1 Like

Let me restart. The template I gave you works, so if you do not want to follow that due to rewriting the entire system, I understand. However, here are my questions then:

  1. Does all your self-values work?
  2. The problem was when you were constructing the mark with Trove (I assumed), is that true?
1 Like

I mainly don’t want to use it since it’s almost the same to what I have which is why it’s confusing that it’s not working

Yep!

Mark doesn’t use trove, the issues I’m having are that no class methods are working (for Trove or for Block)

1 Like

If you did my module script template the same, I assume that where you are calling it is wrong.

-- In a server script.
local Block = require(script.ModuleScript)
local CreateBlock = Block.new()
local Part = CreateBlock:mark()
Part.Parent = workspace

task.wait(5)

CreateBlock:Clean()

Is this kind of how you are calling it?

1 Like

In my code it’s more like

local mark = Block.mark(...)
local block = Block.new(mark)
-- later on
block:Destroy() -- errors

The whole mark system is there in the first place for saving purposes

2 Likes

I am not sure, every solution comes from the code I provided. Sorry if this was no help.

1 Like