Skip to content

Universal Method Finder

Euphoriah edited this page Jun 10, 2024 · 8 revisions

Or Shortly UMF

Source Code

local globalenv = getgenv and getgenv() or _G or shared
local globalcontainer = globalenv.globalcontainer

if not globalcontainer then
	globalcontainer = {}
	globalenv.globalcontainer = globalcontainer
end

local genvs = { _G, shared } -- Could become an issue if the latest function it gets is unaccessible by client
if getgenv then
	table.insert(genvs, getgenv())
end
-- if getrenv then -- Add this if you wish to search through game's env (normal Scripts)
-- 	table.insert(genvs, 1, getrenv())
-- end
-- if debug and debug.getregistry then
-- 	table.insert(genvs, 1, debug.getregistry()._LOADED) -- Includes things like string / table / math library tables etc. Basically everything roblox offers, but due to being too huge you should specify library tables by attaching ".string" or to LOADED; adding "_G" to this will link a globals table. That way the finder will have to scan way less.
-- end

local calllimit = 0
do
	local function determineCalllimit()
		calllimit = calllimit + 1
		determineCalllimit()
	end
	pcall(determineCalllimit)
end

local depth, printresults, hardlimit, query, antioverflow -- prevents infinite / cross-reference
local function recurseEnv(env, envname)
	if globalcontainer == env then
		return
	end
	if antioverflow[env] then
		return
	end
	antioverflow[env] = true

	depth = depth + 1
	for name, val in next, env do
		local Type = type(val)

		if Type == "table" then
			if depth < hardlimit then
				recurseEnv(val, name)
			else
				-- warn("almost stack overflow")
			end
		elseif Type == "function" then -- This optimizes the speeds but if someone manages (??) to fool this then rip
			name = string.lower(tostring(name))
			local matched
			for methodname, pattern in next, query do
				if pattern(name, envname) then
					globalcontainer[methodname] = val
					if not matched then
						matched = {}
					end
					table.insert(matched, methodname)
					if printresults then
						print(methodname, name)
					end
				end
			end
			if matched then
				for _, methodname in next, matched do
					query[methodname] = nil
				end
			end
		end
	end
	depth = depth - 1
end

local function finder(Query: table, ForceSearch: boolean, CustomCallLimit: number, PrintResults: boolean): nil
	antioverflow = {}
	query = {}

	do -- Load patterns
		local function Find(String, Pattern)
			return string.find(String, Pattern, nil, true)
		end
		for methodname, pattern in next, Query do
			if not globalcontainer[methodname] or ForceSearch then
				if not Find(pattern, "return") then
					pattern = "return " .. pattern
				end
				query[methodname] = loadstring(pattern)
			end
		end
	end

	depth = 0
	printresults = PrintResults
	hardlimit = CustomCallLimit or calllimit

	recurseEnv(genvs)

	hardlimit = nil
	depth = nil
	printresults = nil

	antioverflow = nil
	query = nil
end

return finder, globalcontainer

Documentation

Query being a table type (dictionary in this case) uses values (strings) to match names of functions inside different environments. It performs "loadstring" on the pattern and passes the name of the function as an argument to the loadstring. If name of the function matches the pattern (logical statement) you provided as string then it will write said function under the name (key) you provided inside any of these 3 environments (depending on which one is available): getgenv, _G, shared.

Structure in short:

Query (table)

Key - a name that you want the function to have inside global environment (either getgenv, _G or shared) once it is found.
Value (string) - a logical statement that will be put inside loadstring along with name of the current function being checked.

ForceSearch (boolean) - forces search of already found methods therefore overwriting them with freshly found methods(if found that is).
CustomCallLimit (number) must be less than the current depth of the script (or less than 20,000 - 3 in most cases unless function nesting and/or recursion takes place, pcalls, xpcalls, loadstrings, coroutines count too!)
PrintResults (boolean) - prints matched results.

Usage

So let's say you want to find methods for gethiddenproperty & queue_on_teleport, to do so you are supposed to figure out the logical statements that match only the methods you need. It is mostly trial and error to figure out, but general advice is:

  • Split / Spread out your string patterns as much as possible (for example, instead of (...):find("gethiddenproperty", nil, true) you are supposed to do (...):find("get", nil, true) and (...):find("hid", nil, true) and (...):find("prop", nil, true)). Otherwise you are destroying the point of this script (unless you really need to match "gethiddenproperty" string exactly for some reason. In case you are wondering (...) is basically the name of the function that's being currently checked.
  • Make sure the strings you provide for "find" method are all lowercase.
  • Make sure the strings you provide are all a one liner so that they actually act as a string (no newlines). This is mostly a concern if you are using a complex code variant for matching like for the queue_on_teleport. To achieve that you could use a minifier for Lua or LuaU, or if you prefer a Plugin for Visual Studio Code which supports both libraries listed previously. They will allow you to minify your code into a one-liner format which is suitable for strings.
  • Function names vary per executor which is why this script was made. For that matter you are supposed to visit documentations of executors that you want to support. Another good source of names are other compatibility scripts on sites like v3rmillion, as they usually work by spamming all names in fashion of local s = syn and syn.write_clipboard or write_clipboard or clipboard and clipboard.set or copystring or setclipboard or toClipboard or toclipboard or set_clipboard or Clipboard and Clipboard.set. For example scripts like these include a lot of names IrisBetterCompat, Infinite Yield or Hydroxide. Another method is to search Github-wide for your functions' names and see what else people use.
  • After figuring out the part above, you need to find the similarity between various names of same method that you want to find. And then convert it to a logical statement that would match ONLY that method successfully.
  • Sometimes "debug" library and other Roblox libraries might have the methods you need (usually executors add them back if Roblox removed something). You are supposed to set them before using the finder. This way they will be used in case method finder does not find anything better! YOU MUST SET ForceSearch TO TRUE or ASSIGN METHODS A NEW NAME WHEN PROMPTING FINDER IF YOU ARE PRE-DEFINING METHODS (example in code below) (E.G. globalcontainer.getrawtable = debug.getmetatable)
  • Environment names are also passed into your logical statement strings as second argument of the ... Tuple. And can be accessed as local arg2 = ({ ... })[2]. Take a look at the hash logical statement string which matches hash function of the crypt library (environment). Be careful when matching environments as their names can have types other than string so make an if check for it or use tostring().
  • Enable PrintResults (pass "true" as second parameter to finder) when testing to see what names it matches.
  • In the case of queue_on_teleport, you can see how complex the logical statement is. It is because KRNL has a method called run_teleport_queue_scripts which we do NOT want to match with our finder because that function does not do what we are searching for (queue_on_teleport in this case). When using this variant **make sure there is at least one return that returns the match result in the code you provide otherwise the finder will put a return at the start of the logical statement code string which will likely break your code. The non-minifed version of the code for matching (example, can be optimized) queue_on_teleport:
local arg1 = ({ ... })[1]
local result = (arg1):find("queue", nil, true) and (arg1):find("tele", nil, true)

if result and KRNL_LOADED then
	print("Script is being ran on KRNL, performing an extra check")
	result = not ((arg1):find("run", nil, true) or (arg1):find("script", nil, true))
end

return result

Try to avoid using these in logical statement strings:

  1. Complex variant of code, unless you truly believe there is no other way to match whatever you need
  2. Methods that have not been found / defined yet. Also, methods that belong exclusively to some exploit like getsynasset (notice the syn in the name), because it destroys the point of using this finder in the first place. Unless it's something like KRNL_LOADED to only detect KRNL.

Notes

  • ... is a vararg meaning it is a Tuple. It can be packet into an array using {...} (faster) or table.pack(...) and then have it's arguments accessed as packed[1], packed[2] and so on. The reason we use it is that it's the only way to avoid turning the logical statement string into a complex conditional (logical statement) function. The reason we don't need to pack it is because of how tuples work, when you perform actions on ... without packing it then the actions will be only performed on the first argument inside the tuple (if present).
  • :find("lowercase_string", nil, true) looks like that is because of the optimization that 3rd parameter (plain) brings. It basically disables the "magic" pattern-matching characters inside of the string and the string is treated as-is. If you are familiar with patterns like "%d" for matching any digit (number) you'll know what this means. More information at find documentation and String Pattern Reference HOWEVER, if you do wish to match strings with the use of those patterns you must make you finds like these :find("lowercase_pattern_string")
local FileName = "UniversalMethodFinder"
local finder, globalcontainer =
	loadstring(game:HttpGet("https://raw.githubusercontent.com/luau/SomeHub/main/" .. FileName .. ".luau", true), FileName)()
-- globalcontainer can also be indexed using "local container = (getgenv and getgenv() or _G or shared).globalcontainer"

-- Below is the example how sometimes "debug" library and other Roblox libraries might have the methods you need (usually executors add them back if Roblox removed something). You are supposed to set them before running this script aka before the loadstring. This way they will be used in case method finder does not find anything better! Pretty good isn't it.
-- If you do use the Roblox libraries then you need to set to true the ForceSearch boolean parameter or you can assign methods a new name when using finder. If you do not wish to enable the ForceSearch parameter then you must assign them after you've used the finder and if nothing was found then assign the values from the "debug" library in this case - example for this is at the bottom of the code. If you pick this variant then you must remove the similar lines of code at the top as having both is pointless.
if debug and debug.getmetatable then
	globalcontainer.getrawtable = debug.getmetatable
end

finder({
	getrawtable = '(...):find("get", nil, true) and (...):find("raw", nil, true) and (...):find("tab", nil, true)',
	gethiddenprop = '(...):find("^get") and (...):find("hid", nil, true) and (...):find("prop", nil, true)', -- An example of logical statement string that makes use of the string pattern syntax (:find("^get"), `^` being an Anchor and declaring the start of the string meaning get must be at the very start to match)
	queue_on_teleport = 'local arg1 = ({ ... })[1] local result = (arg1):find("queue", nil, true) and (arg1):find("tele", nil, true) if result and KRNL_LOADED then print("Script is being ran on KRNL, performing an extra check") result = not ((arg1):find("run", nil, true) or (arg1):find("script", nil, true)) end return result', -- An example of a complex logical statement that requires more than one of code in order to achieve the goal result which otherwise might not be possible. Usually, you wouldn't need this unless you can't fit your logical statement into one "if" line and require extra control of the matching process
	hash = 'local args = { ... }  local arg1 = args[1] local arg2 = tostring(args[2]) local result = (arg2):find("crypt", nil, true) and (arg1):find("hash", nil, true) return result', -- A good example of matching by the environment name (`crypt` in this case). Be careful when matching environments as their names can have types other than string so make an if check for it or use tostring().
}, true, nil, true) -- ForceSearch = true, PrintResults = true, generally the second one is not needed if you don't need to find the methods again for some reason. It MUST BE SET TO TRUE if you are pre-defining a fallback from Roblox library like debug (e.g. debug.getmetatable), otherwise it will not try to find anything better because the method has already been defined.
-- If your executor supports getgenv() you could remove the part up until the dot. So only leave the function in that case.
-- WARNING: Sometimes methods might be NOT found and therefore nil so you should check if they exist before using them!

-- For people who don't want to use ForceSearch boolean parameter but still want to assign a fallback value in case finder didn't find anything. If you pick this variant then you must remove the similar lines of code at the top as having both is pointless.
if not globalcontainer.getrawtable and debug and debug.getmetatable then
	globalcontainer.getrawtable = debug.getmetatable
end
local gm = globalcontainer.getrawtable(game)
globalcontainer.gethiddenprop(workspace.Terrain, "Decoration", false)
globalcontainer.queue_on_teleport('print("queue ran!")')

Test Siderbar

Clone this wiki locally