Metamethod Hook Examples

Lets now expand on the example script that we were shown in 3.2 and add some actual functionality for it.

local LocalPlayer = game:GetService("Players").LocalPlayer
local GameMt = getrawmetatable(game)
local OldIndex = GameMt.__index

setreadonly(GameMt, false)

GameMt.__index = newcclosure(function(Self, Key)
    if not checkcaller() and Self == LocalPlayer and Key == "Character" then
        return nil
    end

    return OldIndex(Self, Key)
end)

setreadonly(GameMt, true)

Let us now explain the new lines we added.

local LocalPlayer = game:GetService("Players").LocalPlayer

This will grab the LocalPlayer for later on in the script.

if not checkcaller() and Self == LocalPlayer and Key == "Character" then
    return nil
end

This line has a few checks, but most importantly a call to checkcaller. checkcaller allows you to see if the thread calling your hook is a Synapse X thread or not, and modify its behavior based on that. This is really important for many hooks, as most of the time you do not want a hook to be executed if on a Synapse X thread.

If that check passes (not a Synapse X thread), we then check if they were trying to index a LocalPlayer with the field Character, then return nil.

Lets now try that script that we previously explained on the metatables page:

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

If we execute it within a game LocalScript, we get the result we wished:

Nil in Console

Voila! We have successfully intercepted a metamethod index and replaced it with our own values. The possibilities that we can do with this now are near limitless.

__namecall Hook Examples

Lets now do a hook for the __namecall method, based on the same script as before.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local GameMt = getrawmetatable(game)
local OldNameCall = GameMt.__namecall

setreadonly(GameMt, false)

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

    if not checkcaller() and Self == game and NamecallMethod == "GetService" and Args[1] == "Workspace" then
        return ReplicatedStorage
    end

    return OldNameCall(Self, ...)
end)

setreadonly(GameMt, true)

Let us now explain the new lines we added.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

This will grab the ReplicatedStorage service for later on in the script.

local Args = {...}

This will get a list of arguments to the function called.

if not checkcaller() and Self == game and NamecallMethod == "GetService" and Args[1] == "Workspace" then
    return ReplicatedStorage
end

The same checkcaller check applies here, but we have a few more checks within this conditional:

  • The Self == game check will make sure the object passed is game (this prevents someone from doing workspace:GetService("Workspace") and getting the hooked result)
  • The NamecallMethod == "GetService" check will make sure the function being called is GetService.
  • The Args[1] == "Workspace" check will make sure the first argument passed is Workspace.

If all of those conditions pass, we return ReplicatedStorage instead of Workspace.

Lets run an example script to show this

print(game:GetService("Workspace"))

If we execute it within a game LocalScript, we get the result we wanted again:

Nil in Console

Lets now move on to function hooking.