Skip to content

Instantly share code, notes, and snippets.

@A1-exe
Last active February 12, 2023 11:45
Show Gist options
  • Save A1-exe/0c99361ca94050477728759b5eeb0d66 to your computer and use it in GitHub Desktop.
Save A1-exe/0c99361ca94050477728759b5eeb0d66 to your computer and use it in GitHub Desktop.
dank metatable memes. An api for hooking (straight up owning) any instance-related thing in the client of that one blocky game.
--[[
Last Updated: 7/21/2021 12:25 AM
Last Change: Updated to include new synapse hookmetamethod
[
- `__len` (# operator) returns raw object from wrapped objects
- #obj returns the unwrapped version of the object
- `__call` returns the __self table
Ex: __self = obj()
- `__self.caller` return the level of the calling function
- `__self.hook` returns the current hookfunction's table
- `__self.hookparent` returns the parent table of the hook
]
LOADING EXAMPLE:
[
Loader Code
if not __hooks then
----- LOAD HOOK API -----
local Script = game:HttpGet('https://gist.githubusercontent.com/A1-exe/0c99361ca94050477728759b5eeb0d66/raw/', true)
getgenv().__hooks = loadstring(Script)()
-------------------------------
end
]
---------------------------------------------------
-- >> DOCUMENTATION << --
---------------------------------------------------
[
** MOST IMPORTANT **
** UNSET THE SAME WAY YOU USED SET
* Ex: __hooks.set(Inst, 'Prop', hookfunction)
* __hooks.unset(Inst, 'Prop')
* OR
* __hooks.set(Inst.Method, hookfunction)
* __hooks.unset(Inst.Method)
** NOTES **
* One hook per property/function
* You can create a blank hook instead of using the setwrite function
* MethodHooks refer to any property/function that returns a function
(i.e. if it returns a function.. it's considered a method hook)
* If a hookfunction isn't passed to a regular hook... It returns the value, if the value isn't the same type, it returns the real value
* tostring hooking only works on instances
* `Name` property hooks automatically include tostring hooks
]
[
Example Handler Args
** Regular Hook **
* Inst <The Current Object>
* Value <The Real Property Value>
* newValue <Property Value Being Attempted>
*
Ex: handler(Inst, Value, newValue)
* Method Hook *
* Inst <The Current Object>
* realFunction <The Detected Function>
* ...*Args* <A tuple of passed arguments>
*
Ex: handler(Inst, detFunction, ...*Args*)
]
[
Example Hook Table
{
[1] = *function* < The hooking function >,
[2] = *boolean* < isHookDisabled >,
[3] = *boolean* < writeDisabled (Can this property be overwritten)) >,
[4] = *function* < a tracking function >
[5] = *function* < function before hookfunc >
[6] = *function* < function to hook for unset >
MainToggle = *boolean*,
MainWrite = *boolean*
}
Notes:
* MainToggle is used instead of `[2]` when no specific property is provided
* MainWrite is used instead of `[3]` when no specific property is provided
* ^ These are used to disable all hooks/writing to properties of one Inst/Class
]
(Raw Functions | Bypass Hooking API)
:: rawget
- Instance [Inst]
- Property [Str]
:: rawset
- Instance [Inst]
- Property [Str]
- Value [ALL]
:: rawcall
- Instance [Inst]
- Arguments [...*Args*]
- Method [Str]
(API Functions)
[
Examples (get):
get(Instance)
]
:: get (Get a hook's table)
- Instance [Inst]
ClassName [Str]
Method [Func]
- Property [Str] (Optional)
[
Examples (unset):
set(Instance, property)
set(Instance, {properties})
set(ClassName, property)
set(ClassName, {properties})
set(Function, hookfunction)
]
:: unset (Removes a hook) [ returns if a hook was removed ]
- Instance [Inst] | ClassName [Str] | Method [Func]
- Property [Str] | PropertyList [Array]
[
Examples (set):
set(Instance, property, hookfunction)
set(Instance, {properties}, hookfunction)
set(ClassName, property, hookfunction)
set(ClassName, {properties}, hookfunction)
set(Function, hookfunction)
]
:: set (Create a hook) [ returns if a hook was created ]
- Instance [Inst] | ClassName [Str] | Method [Func]
- Property [Str] | PropertyList [Array] | MethodHook [Func] (Inst, Func, ...*Args*)
- HookFunction [Func] (Inst, Func, ...*Args*)
[
Examples (toggle)
toggle(Instance, Property, boolean)
toggle(ClassName, Property, boolean)
toggle(Instance, boolean) // Toggles all properties
toggle(Classname, boolean) // Toggles all properties
]
:: toggle (Toggle hooks) [ returns if a hook was toggled ]
- Instance [Inst]
ClassName [Str]
Method [Func]
- Property [Str]
PropertyList [Array]
- Debounce [Bool]
[
Examples (setwrite)
setwrite(Instance, Property, boolean)
setwrite(ClassName, Property, boolean)
setwrite(Instance, boolean) // Toggles all properties
setwrite(Classname, boolean) // Toggles all properties
]
:: setwrite (Toggle writing abilities) [ returns if a hook was modified ]
- Instance [Inst]
ClassName [Str]
- Property [Str]
PropertyList [Array]
- Debounce [Bool]
[
Examples (showtostring)
showtostring(Instance, string)
]
:: showtostring (Mask tostring) [ returns if a hook was created ]
[
Examples (hidetostring)
hidetostring(Instance)
]
:: hidetostring (Unmask tostring) [ returns if a hook was removed ]
-----------------------------------------------
---- >>> END <<< ----
-----------------------------------------------
]]
local debugmode = false
local newcclosure = newcclosure or (function(f) return f end)
local getnamecallmethod = getnamecallmethod or get_namecall_method
local getrawmetatable = (hookmetamethod and (function() return nil end)) or getrawmetatable or debug.getrawmetatable
local setreadonly = function(t, b)
if setreadonly then
setreadonly(t, b)
elseif make_writable then
make_writable(t, not b)
elseif fullaccess then
fullaccess(t, not b)
end
end
if not getrawmetatable then
return "Methods not available."
end
local Metatable = getrawmetatable(game)
local MetatableIndex = Metatable and Metatable.__index
local MetatableCall = Metatable and Metatable.__namecall
local MetatableNew = Metatable and Metatable.__newindex
local MetatableTo = Metatable and Metatable.__tostring
local IsA = game.IsA
if debugmode then
print("LOADING:", (Metatable and "Is not") or "Is", "expiremental.")
print("INDEX:", MetatableIndex)
print("CALL:", MetatableCall)
print("NEW:", MetatableNew)
print("TO:", MetatableTo)
end
local hookfunc = hookfunc or hookfunction
local hooks, Hooks = {},
{
ToStrings = {},
Globals = {},
Funcs = {},
Insts = {}
}
-- Relay types to table names
local function Get(i)
local Type = typeof(i)
return (((Type == "string") and "Globals") or ((Type == "function") and "Funcs") or
((Type == "Instance") and "Insts"))
end
-- Expand arguments to tables and wrap hookfunctions in CClosures
-- Return the last table and it's parent and index so it can be unset
local function Expand(t, ...)
local Args, Ret, RetParent, RetIndex = {...}
local Recurse
do
function Recurse(r, i)
if not Args[i] then
return Ret, RetParent, RetIndex
end
if not r[Args[i]] then
r[Args[i]] =
setmetatable(
{},
{
__newindex = function(s, i, v)
if (i == 1) and (typeof(v) == "function") then
rawset(s, i, newcclosure(v))
else
rawset(s, i, v)
end
end
}
)
end
RetParent = r
RetIndex = Args[i]
Ret = r[Args[i]]
return Recurse(Ret, i + 1)
end
end
return Recurse(t, 1)
end
-- Wrap instances to prevent infinite loop
local WrapRaw;
WrapRaw = newcclosure(function(obj, func_self)
local newobj = newproxy(true)
local newmeta = getmetatable(newobj)
if (typeof(obj) ~= 'Instance') then
error('invalid argument #1 (Instance expected, got '..typeof(obj))
end
-- // Use `__len` before in comparisons
newmeta.__len = newcclosure(function(self)
return obj
end)
newmeta.__call = newcclosure(function(self, ...)
return func_self
end)
newmeta.__tostring = newcclosure(function()
return tostring(obj)
end)
newmeta.__newindex = newcclosure(function(self, index, value)
hooks.rawset(obj, index, value)
end)
newmeta.__index = newcclosure(function(self, index)
local ret = hooks.rawget(obj, index)
if (typeof(ret) == 'Instance') then
return WrapRaw(ret, func_self)
end
if (typeof(ret) == 'function') then
return function(arg1, ...)
if (arg1 == self) then
return ret(obj, ...)
else
return ret(obj, arg1, ...)
end
end
end
return ret
end)
return newobj
end)
-- Add self to function
local function MakeSelf(tab, par, caller)
return {
caller = caller,
hookparent = par,
hook = tab,
}
end
----------------------------------------
--------------- HOOK API ---------------
----------------------------------------
hooks.get =
newcclosure(
function(Index, Property)
local Act = Get(Index)
if not Act then
return
end
local Target = Hooks[Act]
if Act then
return Expand(Target, Index, Property)
end
end
)
hooks.unset =
newcclosure(
function(Index, Props)
local Action, Act = typeof(Props), Get(Index)
if not Act then
return
end
if ((Act == "Globals") or (Act == "Insts")) and (Action == "table") then
for __, Property in next, (Props) do
local HookTable, HookTableParent, HookTableIndex = hooks.get(Index, Property)
if HookTable[5] and HookTable[6] then
hookfunc(HookTable[6], HookTable[5])
end
HookTableParent[HookTableIndex] = nil
end
return true
elseif ((Act == "Globals") or (Act == "Insts")) and (Action == "string") then
local HookTable, HookTableParent, HookTableIndex = hooks.get(Index, Props)
if HookTable[5] and HookTable[6] then
hookfunc(HookTable[6], HookTable[5])
end
HookTableParent[HookTableIndex] = nil
return true
elseif (Act == "Funcs") and (Action == "function") then
local HookTable, HookTableParent, HookTableIndex = hooks.get(Index)
if HookTable[5] and HookTable[6] then
hookfunc(HookTable[6], HookTable[5])
end
HookTableParent[HookTableIndex] = nil
return true
end
end
)
hooks.set =
newcclosure(
function(Index, Props, Func)
local Action, Act = typeof(Props), Get(Index)
if not Act then
return
end
if ((Act == "Globals") or (Act == "Insts")) and (Action == "table") then
for __, Property in next, (Props) do
local HookTable = hooks.get(Index, Property)
HookTable[1] = (function()
if typeof(Func) == "function" then
if (Act == "Insts") and (Property == "Name") then
hooks.showtostring(Index, Func())
end
return Func
else
if (Act == "Insts") and (Property == "Name") then
hooks.showtostring(Index, Func)
end
return function()
return Func
end
end
end)()
end
return true
elseif ((Act == "Globals") or (Act == "Insts")) and (Action == "string") then
local HookTable = hooks.get(Index, Props)
HookTable[1] = (function()
if typeof(Func) == "function" then
if (Act == "Insts") and (Props == "Name") then
hooks.showtostring(Index, Func())
end
return Func
else
if (Act == "Insts") and (Props == "Name") then
hooks.showtostring(Index, Func)
end
return function()
return Func
end
end
end)()
return true
elseif (Act == "Funcs") and (Action == "function") then
-- Prevent cyclical `Instance[<method>]` bafoonery when indexing hooked functions.
-- Returned address for indexed funcitons change once
for hookedfunc, hookedtab in next, (Hooks.Funcs) do
if (hookedfunc == Index) or (hookedtab[4] == Index) then
Index = hookedfunc
break
end
end
local HookTable = hooks.get(Index)
HookTable[1] = Props
return true
end
end
)
hooks.toggle =
newcclosure(
function(Index, ...)
local Args, Act = {...}, Get(Index)
local Action = typeof(Args[1])
if not Act then
return
end
if ((Act == "Globals") or (Act == "Insts")) and (#Args > 1) and (Action == "table") then
for __, Property in next, (Args[1]) do
local HookTable = hooks.get(Index, Property)
HookTable[2] = not Args[2]
end
return true
elseif (typeof(Act) == "string") and (#Args == 1) then
local HookTable = hooks.get(Index)
HookTable.MainToggle = not Args[1]
return true
elseif (Action == "string") and (#Args > 1) then
local HookTable = hooks.get(Index, Args[1])
HookTable[2] = not Args[2]
return true
end
end
)
hooks.rawget =
newcclosure(
function(Obj, Index)
local Return = MetatableIndex(Obj, Index)
if (type(Return) == "function") then
local FuncHook = Hooks.Funcs[Return]
if FuncHook and FuncHook[5] then
return FuncHook[5]
end
local InstHook = Hooks.Insts[Obj]
if InstHook and not InstHook.MainToggle then
local Prop = InstHook[Index]
if Prop and Prop[5] then
return Prop[5]
end
end
for Class, Hook in next, (Hooks.Globals) do
if IsA(Obj, Class) and not Hook.MainToggle then
local ClassHook = Hook[Index]
if ClassHook and ClassHook[5] then
return ClassHook[5]
end
end
end
end
return Return
end
)
hooks.rawset =
newcclosure(
function(...)
MetatableNew(...)
end
)
hooks.rawcall =
newcclosure(
function(...)
return MetatableCall(...)
end
)
hooks.setwrite =
newcclosure(
function(Index, ...)
local Args, Act = {...}, Get(Index)
local Action = typeof(Args[1])
if not Act then
return
end
if ((Act == "Globals") or (Act == "Insts")) and (#Args > 1) and (Action == "table") then
for __, Property in next, (Args[1]) do
local HookTable = hooks.get(Index, Property)
HookTable[3] = not Args[2]
end
return true
elseif (typeof(Act) == "string") and (#Args == 1) then
local HookTable = hooks.get(Index)
HookTable.MainWrite = not Args[1]
return true
elseif (Action == "string") and (#Args > 1) then
local HookTable = hooks.get(Index, Args[1])
HookTable[3] = not Args[2]
return true
end
end
)
hooks.showtostring =
newcclosure(
function(Obj, Value)
if (typeof(Obj) == "Instance") and (typeof(Value) == "string") then
Hooks.ToStrings[Obj] = Value
end
end
)
hooks.hidetostring =
newcclosure(
function(Obj)
local StrHook = Hooks.ToStrings[Obj]
if StrHook then
Hooks.ToStrings[Obj] = nil
end
end
)
---------------------------------------------
--------------- HOOK FUNCTIONS --------------
---------------------------------------------
---- MethodHook
local function PropertyHook(Return, Obj, Index)
local IsMethod = (type(Return) == "function")
if (Index == "MainToggle") or (Index == "MainWrite") then
return Return
end
local FuncHook = IsMethod and Hooks.Funcs[Return]
if FuncHook and FuncHook[1] then
if not FuncHook[4] then
local newFunc, oldFunc = function(obj, ...)
return FuncHook[1](WrapRaw(obj, MakeSelf(FuncHook, Hooks.Funcs, 4)), FuncHook[5], ...)
end
oldFunc = hookfunc(Return, newFunc)
FuncHook[4] = newFunc
rawset(FuncHook, 5, oldFunc)
rawset(FuncHook, 6, Return)
end
if not FuncHook[2] then
return FuncHook[4]
else
return FuncHook[5]
end
end
local InstHook = Hooks.Insts[Obj]
if InstHook and not InstHook.MainToggle then
local Prop = InstHook[Index]
if Prop and Prop[1] then
local __self = MakeSelf(Prop, InstHook, 5)
if IsMethod then
if not Prop[4] then
local newFunc, oldFunc = function(obj, ...)
__self.caller = 4
return Prop[1](WrapRaw(obj, __self), Prop[5], ...)
end
oldFunc = hookfunc(Return, newFunc)
Prop[4] = newFunc
rawset(Prop, 5, oldFunc)
rawset(Prop, 6, Return)
end
return ((not Prop[2]) and Prop[4]) or Prop[5]
end
if not Prop[2] then
local mask = Prop[1](WrapRaw(Obj, __self), Return)
if (typeof(mask) == typeof(Return)) then
return mask
end
end
return Return
end
end
for Class, Hook in next, (Hooks.Globals) do
if IsA(Obj, Class) and not Hook.MainToggle then
local ClassHook = Hook[Index]
if ClassHook and ClassHook[1] then
local __self = MakeSelf(ClassHook, Hook, 5)
if IsMethod then
if not ClassHook[4] then
local newFunc, oldFunc = function(obj, ...)
__self.caller = 4
return ClassHook[1](WrapRaw(obj, __self), ClassHook[5], ...)
end
oldFunc = hookfunc(Return, newFunc)
ClassHook[4] = newFunc
rawset(ClassHook, 5, oldFunc)
rawset(ClassHook, 6, Return)
end
return ((not ClassHook[2]) and ClassHook[4]) or ClassHook[5]
end
if not ClassHook[2] then
local mask = ClassHook[1](WrapRaw(Obj, __self), Return)
if (typeof(mask) == typeof(Return)) then
return mask
end
end
return Return
end
end
end
return Return
end
---- MethodHook
local function MethodHook(Obj, ...)
local Method = getnamecallmethod()
local MethodFunc = MetatableIndex(Obj, Method)
if (Method == "MainToggle") or (Method == "MainWrite") then
return MetatableCall(Obj, ...)
end
local FuncHook = Hooks.Funcs[MethodFunc]
if FuncHook and FuncHook[1] then
if not FuncHook[4] then
local newFunc, oldFunc = function(obj, ...)
return FuncHook[1](WrapRaw(Obj, MakeSelf(FuncHook, Hooks.Funcs, 4)), FuncHook[5], ...)
end
oldFunc = hookfunc(MethodFunc, newFunc)
FuncHook[4] = newFunc
rawset(FuncHook, 5, oldFunc)
rawset(FuncHook, 6, MethodFunc)
end
if not FuncHook[2] then
return FuncHook[1](WrapRaw(Obj, MakeSelf(FuncHook, Hooks.Funcs, 4)), FuncHook[5], ...)
else
return FuncHook[5](Obj, ...)
end
end
local InstHook = Hooks.Insts[Obj]
if InstHook and not InstHook.MainToggle then
local Prop = InstHook[Method]
if Prop and Prop[1] then
if not Prop[4] then
local newFunc, oldFunc = function(obj, ...)
return Prop[1](WrapRaw(Obj, MakeSelf(Prop, Hooks.Funcs, 4)), Prop[5], ...)
end
oldFunc = hookfunc(MethodFunc, newFunc)
Prop[4] = newFunc
rawset(Prop, 5, oldFunc)
rawset(Prop, 6, MethodFunc)
end
if not Prop[2] then
return Prop[1](WrapRaw(Obj, MakeSelf(Prop, Hooks.Funcs, 4)), Prop[5], ...)
else
return Prop[5](Obj, ...)
end
end
end
for Class, Hook in next, (Hooks.Globals) do
if IsA(Obj, Class) and not Hook.MainToggle then
local ClassHook = Hook[Method]
if ClassHook and ClassHook[1] then
if not ClassHook[4] then
local newFunc, oldFunc = function(obj, ...)
return ClassHook[1](WrapRaw(Obj, MakeSelf(ClassHook, Hooks.Funcs, 4)), ClassHook[5], ...)
end
oldFunc = hookfunc(MethodFunc, newFunc)
ClassHook[4] = newFunc
rawset(ClassHook, 5, oldFunc)
rawset(ClassHook, 6, MethodFunc)
end
if not ClassHook[2] then
return ClassHook[1](WrapRaw(Obj, MakeSelf(ClassHook, Hooks.Funcs, 4)), ClassHook[5], ...)
else
return ClassHook[5](Obj, ...)
end
end
end
end
return MetatableCall(Obj, ...)
end
---- StringHook
local function StringHook(Obj)
local StrHook = Hooks.ToStrings[Obj]
if StrHook then
return StrHook
end
return MetatableTo(Obj)
end
------------------------------------------------------
--------------- ATTACH METATABLE HOOKS ---------------
------------------------------------------------------
if Metatable then
setreadonly(Metatable, false)
end
----- NAMECALL -----
local TrueNameCallHook = (MethodHook)
if not Metatable then
MetatableCall = hookmetamethod(game, "__namecall", TrueNameCallHook)
else
Metatable.__namecall = newcclosure(TrueNameCallHook)
end
----- TOSTRING -----
local TrueToStringHook = (StringHook)
if not Metatable then
MetatableTo = hookmetamethod(game, "__tostring", TrueToStringHook)
else
Metatable.__tostring = newcclosure(TrueToStringHook)
end
----- INDEX -----
local TrueIndexHook = (
function(Parent, Index)
local Return = MetatableIndex(Parent, Index)
return PropertyHook(Return, Parent, Index)
end
)
if not Metatable then
MetatableIndex = hookmetamethod(game, "__index", TrueIndexHook)
else
Metatable.__index = newcclosure(TrueIndexHook)
end
----- NEWINDEX -----
local TrueNewIndexHook = (
function(Obj, Index, Value)
local Success, Return = pcall(function()
return MetatableIndex(Obj, Index)
end)
if not Success or (typeof(Return) ~= typeof(Value)) then
return MetatableNew(Obj, Index, Value)
end
local IsMethod = (typeof(Return) == 'function')
local InstHook = Hooks.Insts[Obj]
if InstHook then
local Prop = InstHook[Index]
-- If hook exists call it instead
if Prop and (typeof(Return) == typeof(Value)) and Prop[1] and not Prop[2] then
Prop[1](WrapRaw(Obj, MakeSelf(Prop, InstHook, 4)), (IsMethod and Prop[5]) or Return, Value)
return
end
if InstHook.MainWrite or (Prop and Prop[3]) then
return
end
end
for Class, Hook in next, (Hooks.Globals) do
if IsA(Obj, Class) then
local ClassHook = Hook[Index]
-- If hook exists call it instead
if ClassHook and (typeof(Return) == typeof(Value)) and ClassHook[1] and not ClassHook[2] then
ClassHook[1](WrapRaw(Obj, MakeSelf(ClassHook, Hook, 4)), (IsMethod and ClassHook[5]) or Return, Value)
return
end
if Hook.MainWrite or (ClassHook and ClassHook[3]) then
return
end
end
end
return MetatableNew(Obj, Index, Value)
end
)
if not Metatable then
MetatableNew = hookmetamethod(game, "__newindex", TrueNewIndexHook)
else
Metatable.__newindex = newcclosure(TrueNewIndexHook)
end
---------------------------------------
--------------- CLEANUP ---------------
---------------------------------------
if debugmode then
print("LOADED.")
print("INDEX:", MetatableIndex)
print("CALL:", MetatableCall)
print("NEW:", MetatableNew)
print("TO:", MetatableTo)
end
if Metatable then
setreadonly(Metatable, true)
end
if debugmode then
getgenv().__hooks = hooks
end
return hooks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment