Allow Metatable hooks on ROBLOX Services or Instances (Sorta...)

1. Introduction


As a Roblox developer, it is currently too hard to - if not impossible - to hook in to ROBLOX functions.

So, let’s talk about this. When reading this you may be thinking any of the following: “security risk!”, “useless feature!”, “inheritance (sort of) works!”

All of these things will be addressed within this post, and hopefully I can convince you (if I need too) as to why this is a potentially good idea.

Please note that there may be underlying edge cases as to why this is not already possible- ones in which we may not know about due to lack of engine access.

2. Use Case


A commonly found game setup within managed frameworks would be to have a controller in which handles particular services provided by ROBLOX. Let’s use the example of “Players”

When a player joins your game, you then assign a new class to the given player, this can be done one of two ways.

Composition

The use of assigning a property within the class itself which is a pointer to some dependant.

function controller.new(player : Player)
   local self = setmetatable({}, controller)
   self.instance = player
end

Inheritance

Making your new metatable have an index pointer to the instance

function controller.new(player : Player)
   -- Yes, there are better ways of doing this, however this also serves as
   -- a way to potentially hook argument passes- which is why I took this approach for the example.
   local self = setmetatable({}, {
      __index = function(self, index)
         -- Would want to do a check to ensure the index is a func.
         return function(self, ...)
            if player[index] then return player[index](self, ...)
            return controller[index](self, ...)
         end
      end
   })
end

The Problem

So, both of these work, so what exactly is the problem? Well the main problem is that this setup (specifically the inheritance) will not allow you to pass through ROBLOX services or instances (ie; events… etc.)

This effectively means you need to create wrappers for anything that the engine provides you, alternatively you need to constantly reference some cached instance. On paper this doesn’t seem horrible, but it becomes an extreme inconvenience, especially when you’re handling multiple different services, components, etc…

It’d be much nicer to directly attach whatever functionalities you need to a component of your game, wouldn’t it?

Say you have a model in your game, you first need to reference your controller for that model, you then need to find the model from that controller, then you need to run your functions on the given component. This quickly becomes really annoying!

3. The Underlying Issues


Security & Game Breaking Issues

So, let’s say you could hook methods. How can we now prevent people from breaking- say core reliant functionalities? Such as a block feature. This is a flaw, of course.

Certain methods that are could potentially be a security risk to override (ie core features, etc…)- could be immutable. They could always reference back to the original function, regardless of what you may declare within your hooks.

The proposed solution, as seen below, also solves both security and breaking game issues by introducing a new method to the base Instance class, one in which does not allow you to override or hook functionalities, rather allow you to add your own methods to a pre-existing ROBLOX class / instance.

3. The Solution


Not So Great Solution

Well, the obvious solution would be to allow users to simply read current class metatable as well as assign to it. This is how script executors, etc… (at a base level) have been done it thus far.

This solution would look something like the following:
This may have inaccuracy, I wouldn’t be surprised if it did.

local players = game:GetService("Players")
local meta = getmetatable(players.PlayerAdded)

setmetatable(players.PlayerAdded, {
   __index = function(self, index)
      -- Some sort of functionality here, where self
      -- would be a direct reference to the RBLXScriptConnection
      return meta[index]
   end
})

Now this rough example is NOT what I recommend. It has the problems aforementioned! This could break a lot of core functionalities the engine is reliant on!

My Proposed Solution

Rather then allowing developers to directly hook instances. I propose a new method on the base Instance. Names could obviously be subject to change for a better suited fit.

Instance:GetInstanceRegistry()
Returns an empty metatable associated with the instance.

Instance:SetInstanceRegistry(registry : table)
Set’s the instances registry to the provided table.

Obviously these names aren’t perfect, and this use case may not be perfect. But this would allow you to directly attach a table of information, with metadata, to an instance!

Now, if we really wish to go one step further, we could have a hierarchy of execution. Let’s say a developer wishes to add their own methods to a Player class, any methods called on the class would first go through ROBLOX’s engine, and then it would go to the developers associated registry. Don’t worry, examples below!

Examples

Here are some proposed examples of how this would look!

Basic Example

-- A player variable, this could be anything realistically.
local player = ...

-- We have a basic class!
local controller = {}
controller.__index = controller

function controller:Kill()
   -- The self reference would be our player!
   self.Character.Humanoid.Health = 0
end

local registry = player:GetInstanceRegistry()
local metatable = setmetatable(registry, controller)

-- Finally, assign this new metatable information to the instance once more.
player:SetInstanceRegistry(metatable)
-- This would be a different script for example.
-- A player variable once more.
local player = ...

-- Ideally we could directly call the methods, and if it doesn't exist on the
-- base instance, it would check our registry for it.
player:Kill()

-- Alternatively...
player:GetInstanceRegistry():Kill()

Collision Example
What happens when the clause in our proposed solution happens? Where we add a pre-existing method? Well the engine would always take priority!

-- A player variable, this could be anything realistically.
local player = ...

-- We have a basic class!
local controller = {}
controller.__index = controller

-- But wait! This is a method already existing in ROBLOX's Player Class!
function controller:Kick()
   print("This print would happen after ROBLOX processes it's kick function!")
end

local registry = player:GetInstanceRegistry()
local metatable = setmetatable(registry, controller)

-- Finally, assign this new metatable information to the instance once more.
player:SetInstanceRegistry(metatable)

player:Kick() -- Still kicks them from the game, then afterwards it does our print!

Door Example
Here is an example of how easy components become in our game!

-- Door Controller Script
local collectionService = game:GetService("CollectionService")
local doors = collectionService:GetTagged("Doors")

local controller = {}
controller.__index = controller

function controller:Open(player)
   -- Oh hey! Our player has a registry to ensure it has a key!
   if player:HasKey(self) then
      -- Not entirely sure why this would be neccessary for a door, specifically
      -- firing the given client; maybe a sound. But this is to showcase that we
      -- have custom methods on a class (HasKey), and can still pass our class / instance
      -- to any engine instances, methods, etc.

      event:FireClient(player, true)
   end
end

for _, door in pairs(doors) do
   local registry = door:GetInstanceRegistry()
   local meta = setmetatable(registry, controller)
   door:SetInstanceRegistry(meta)
end
-- This is a script inside of your DOOR PART / MODEL!
local players = game:GetService("Players")
local door = script.Parent

door.Touched:Connect(function(part)
   local player = players:GetPlayerFromCharacter(part.Parent)
   door:Open(player)
end)
18 Likes

this just seems too much of a security risk to be worth implementing imo

1 Like

Hey! I recommend both of you re-read the thread, specifically the part in which I mention security risks!

If you have any examples as to how the proposed solution could be used for malicious purposes, please let me know!

Hooking in to pre-existing classes has nothing to do with exploiting, and has been a thing in other languages for quite some time- ie; Prototypes, etc.

8 Likes

How is that a security risk unless you use all the toolbox models you see?

4 Likes

because people are gona use toolbox models lol

Still failing to see how this is a security risk? Can you please give an example.

4 Likes

Now that i realize this would be great addition, imagine adding new methods to classes or even overwrite them entirely (altrough some methods perhaps can’t be overridden like Kick, altrough even if you could i’d imagine Roblox would still disconnect a client trough other means should something happen like Player instance being destroyed)

hmm let me think, person overrides screenshot function of coregui to steal cookie, person takes screenshot boom they lost their cookie

The screenshot function isn’t a core feature, nor does it store your cookie. Furthermore the proposed solution doesn’t let you override- only implement your own hooks as a layer on top of pre-existing ones.

Please read the thread before posting responses, it’s clear you didn’t.

10 Likes

The only security risk I can see is logging http requests or marketplace functions (which you probably dont even have if you use free models)


uh uh sure bud

The screenshot functionality once again does not have your cookie- it has write file access as well as the capability to read screen information.

Please stop spreading misinformation. Once again read the post before responding

7 Likes

you clearly dontk now what you are talking about
imagine this
screenshot function can only be run in robloxsecurity identity so when you press screenshot button a corescript will run the screenshot function. if you change the function you can run whatever code you want to in a higher identity

Wow, you actually are just not reading a thing I’m saying- or reading the thread.

I can’t keep arguing/discussing this with someone who refuses to even look at the proposed feature that I’m requesting.

Not only have I mentioned that certain functions/methods/services could be immutable (meaning you cannot write or read them), the solution I proposed does not let you override ANY functionalities!

On top of that, due to the nature of the ROBLOX functionality always taking precedent over the user-made one, you would assume that the developer provided hooks / methods would be returned and executed within the developer provided script security.

This will be my last response, if you decide to read my post and provide proper insights, then feel free to respond.

11 Likes

I’m not sure the viability of the proposal due to the luau typechecking. What I mean is that there would be no way to the associated methods of any instance, as those methods are changed dynamically via script. (see the image below)

Outside of that, there’d probably be a lot of lost performance to having something like this implemented.


For the record, you can build a wrapper right into index allowing for the intended functionality of inheritance. See my example below, you can throw that directly into studio.

local BetterPlayer = {}

function BetterPlayer.new(player)
	local self = {}
	self.EpicScore = 100
	
	return setmetatable(self, {
		__index = function(self, key)
			
			if rawget(self, key) then
				return rawget(self, key); -- first check for custom properties
			elseif BetterPlayer[key] then
				return BetterPlayer[key] -- check for shared methods
			elseif typeof(player[key]) == "function" then
				return function(self, ...) -- we're returning the function wrapped with the player as the first argument (works with any instance type)
					return player[key](player, ...)
				end
			else
				return player[key]  -- player properties
			end
		end,
	})
end

function BetterPlayer:Kill()
	self.Character.Humanoid.Health = 0;
end

game.Players.PlayerAdded:Connect(function(player)
	task.wait(2)
	player = BetterPlayer.new(player);
	print(player.EpicScore); -- 100
	task.wait(2)
	print(player:GetChildren()); -- {StarterGear, PlayerGui, Backpack}
	task.wait(2);
	player:Kill(); -- dies
end)
2 Likes

This is definitely a valid concern. I personally don’t have any immediate ideas as to how you could handle type checking system a system like this either- although I wouldn’t rule it out on that fact alone.

There would be next to no performance withdraws from what I can imagine- you simply check if a base instance has additional hooks, if it doesn’t you need no more processing. If it does, then it would theoretically be nothing more then indexing some sort of cache. Obviously I don’t know the in’s and out’s of how the base instances work, but I can’t imagine this would impact performance any more then say- adding a property or attribute would.

This is pretty much a 1:1 of the example I provided earlier in my script- where I also explained why this isn’t helpful for me.

2 Likes

What about just add a Hooks as a new global? And they would work similar to how Gmod Lua hooks does things, they allow you to hook into engine-side functions and run your code before/after an engine-side function ran. Hooking into an engine-side function would also pass ‘self’ as the default parameter, allowing you to assign some custom variables/methods that you can call.

2 Likes

This is a performance tarpit. Good luck getting Roblox to add it. Don’t see why its so hard to just make a prototype that references the Instance.

also whoops just broke every CoreScript by removing a core method it relies on, whoops just broke every admin script by removing the Kick method.

3 Likes

Did you even read the post at all? Others have brought up some valid points but the post clearly states that the default instance method takes precedence over user made ones

3 Likes

Injecting existing functions seems like a bad idea but inheriting from Instances in lua? Maybe .

1 Like