Objects and Metatables

When you create an instance with Instance.new, internally it creates an 'Object'. Every instance in the game is an object - game, workspace, game:GetService("ReplicatedStorage"), or any other instance is an object. There is a cool thing with all of these objects though - they all have the same metatable.

You may be asking - what is a metatable? Fear not, as we will now be diving deep into how metatables work and why they are so important for development with Synapse X.

Metatables

Metatables serve an extremely important purpose in both Lua and Synapse X - they allow for logic to be put behind regular tables, allowing for powerful programming constructs that are extensively used throughout the game engine.

But for Synapse X, they allow something even more powerful - lets look at an example script and explain the process of how it executes:

print(game:GetService("Players").LocalPlayer.Character)

Lua Flow Graph

We will take a particular look at the blocks highlighted in blue - those are metamethod calls, and Synapse X has a special feature for metamethod calls - we can hook them.

Metamethod Hooking

Metamethods are a particular feature of metatables which allow Lua functions to be called when someone tries to do certain operations with our metatable. We will be using that fact to replace the function used for the object metatable.

You may be thinking - "Doesn't getmetatable block you from getting the metatable with a __metatable property defined?" The answer for that question is yes, but Synapse X has a trick up its sleeve for that.

Simple metamethod hook

The following script is the base script used for metamethod hooking on Synapse X. We will explain this script line by line.

local GameMt = getrawmetatable(game)
local OldIndex = GameMt.__index
local OldNameCall = GameMt.__namecall

setreadonly(GameMt, false)

GameMt.__index = newcclosure(function(Self, Key)
    return OldIndex(Self, Key)
end)

GameMt.__namecall = newcclosure(function(Self, ...)
    local NamecallMethod = getnamecallmethod()

    return OldNameCall(Self, ...)
end)

setreadonly(GameMt, true)
local GameMt = getrawmetatable(game)

This is the 'trick up our sleeve' - it bypasses any __metatable property and allows you to directly get the metatable.

local OldIndex = GameMt.__index
local OldNameCall = GameMt.__namecall

We need to create backups of the old functions so can call the originals.

setreadonly(GameMt, false)

This makes the table not read-only, which is required so we can overwrite the metamethods.

GameMt.__index = newcclosure(function(Self, Key)
    return OldIndex(Self, Key)
end)

This has two parts - the newcclosure call and the actual hook itself. This hooks the __index metamethod, which is called whenever someone indexes a object. (game.Workspace for example)

We will be explaining why newcclosure is needed later on, but you need to call it for any function that will be used within a hook of any kind.

GameMt.__namecall = newcclosure(function(Self, ...)
    local NamecallMethod = getnamecallmethod()

    return OldNameCall(Self, ...)
end)

This is the same as the above hook, but will replace __namecall, which is used for calls with self. (game:GetService("Workspace") for example)

The getnamecallmethod call is needed so we can get the function name which we will use later in this tutorial. (in the above case, NamecallMethod would be GetService)

setreadonly(GameMt, true)

This will make the table read only again. This isn't 100% required, but is good form.

We will now be taking a detour to explain how newcclosure works, which is important to understand for later parts of this tutorial.