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)