Hook ANY function in Lua through the use of meta-tables
Intro
Alright, first official submission here on GameBanana; hopefully it will help out some scripters who have met a dead-end feature wise!
Please fully read the "What is this?" section before you move on, as it outlines what exactly I will be explaining!
If you're not specifically looking for this tutorial, it is still a good read, as it opens up possibilities and ideas that you might not have known could exist!
Also, do be warned: I'm the kind of person to over-write, meaning I write sometimes too much ;) If you're going to dive right in, be prepared for a lengthy read!
What is this?
Alright, so in Lua 5.1 (incl. the more recent versions of Lua -- 5.1 is what GMod uses in particular!) a scripter is given the ability to edit tables called "meta tables".
This tutorial teaches you how to redirect a function (we'll target Entity:EmitSound()) in order to control every call made to this function throughout the game.
This tutorial, as mentioned above, will target the EmitSound() function to create a realistic "slow motion" effect (which I will be using in one of my upcoming gamemodes).
Technical Stuff
This section goes in-depth about what exactly is being done here. This part is optional, but will explain why this works, how it works, and ultimately teaches you exactly how Lua works. While it is not required that you know the details in this section, it is highly recommended that you at least skim this section! You can skip to the "The Code" section if you do not intend to read this section!
First off, you should be familiar with "tables", which are used to hold functions and values. So to speak, tables, arrays, and classes in Lua are one in the same (see here for an intro to tables).
Alright, so in essence these "meta tables" are what Lua uses to store the "addresses" (the location in memory [your RAM] where the function starts) to functions defined in a class (table).
Let me give you a little demonstration.
In GMod, we run the following commands:
] lua_run_cl function MyFunc(m) MsgN(m) end
] lua_run_cl MyFunc("hello")
and, obviously, get
hello
Now, where is "MyFunc" stored? What's its address? Simply do:
] lua_run_cl MsgN(MyFunc)
Which will spit out
function: 03689EB8
Alright, we got an address (the 0368... part)! If you understand addresses, then YES, this is the address in actual memory. If you're lost, here's an explanation:
In every programming language that works directly with the processor (using binary instructions; common languages that do this are C++ or C), there are functions and variables. Each function/variable is stored at an "address", or a specific 'seat' in memory. Addresses are generally identified in "hexadecimal" format (hence the letters in the above address) and point to the first byte of the function/variable (Since every function/variable is at least 1 byte long).
Since Lua is just a scripting language that is built on top of larger, more complex systems to ultimately make it easier on the end-user to control (just like how Lua is incorporated into GMod), Lua in turn uses the function addresses that are created by the Source engine at runtime.
Now, if this made any sort of sense to you, you're doing good! If not, keep reading.
Alright cool, so what does this have to do with meta-tables?
If you understood the above explanations, then meta-tables will be easy for you:
Metatables are simply tables (arrays) that define an object (i.e. Entities, a CDamageInfo object, the 'team', 'player', or 'concommand' libraries, etc.) and store the addresses of each function defined within that class.
How about a visual demonstration to make that last paragraph a bit easier, eh? Let's make a class in Lua:
MyClass = {} -- Create the object (table) using '{}'
MyClass.MyFunc = function(m) -- Again, for you over achievers, this way of defining
-- MsgN out m -- a function is called an 'anonymous function';
MsgN(m) -- this is also used in the 'concommand.Add' helpfile on the
end -- garrysmod.com wiki
Now if we run MyClass.MyFunc("hello") then we're obviously going to have "hello" printed to the command line. As we did above, if we run MsgN(MyClass.MyFunc) [WITHOUT ()'s on MyFunc] we're going to get the address.
Now here is where meta-tables come into play. After the above script, we'll put:
PrintTable(MyClass)
What the above command does is pretty obvious: it fetches the metatable of MyClass (the table of functions and their addresses) and then prints that table. The output I received (which will be different every time) is:
MyFunc = function: 0368B280
Now, the more functions we add to MyClass, the more lines that last command will output (See the attachment, which is just the first of two and a half pages of functions defined within the Entity class).
One last command you should know about before I move on: FindMetaTable(). This command is implemented by GMod exclusively (whereas getmetatable is actually built into the Lua system, and carries over to every Lua 5.1 implementation, such as WoW addons) and finds the meta table of almost any basic library or object you could think of; including the Entity class, which is what we'll be using in the next section.
How to Hook a Function
So, like I said above, we're going to be adding onto a 'host_timescale 0.5'-type slow-motion for Garrys Mod by slowing down the pitch on guns and entities to match their animation speed. This means a reload sound of a gun will match up with the animation just fine.
We will be doing this by redirecting the "Entity:EmitSound()" function in order to slow down all sounds by multiplying the host_timescale ConVar with the pitch argument. This makes it easier to manage the slow-motion effect by only changing one convar, and effectively changes the pitch of all sounds depending on the speed of the game.
First, we need to create the callback (what will be called in place of EmitSound) Secondly, we need to actually hook the old EmitSound to tell it to use our new function
The order of operations for the callback (replacement) EmitSound:
- Define a function with four parameters† ('self' object, sound, volume, pitch)
- Within that function, check to see if volume/pitch have been passed a value and if not, assign them the default value††
- Multiply 'pitch' with the host_timescale ConVar
- Pass all of this information to the OLD EmitSound() (See step 1 below)
The order of operations for the hook are as follows:
- Create a function to do the actual hooking (i.e. "HookSound()")
- Get the metatable for Entity (since EmitSound is part of entity and is inherited by all SENTs/SWEPs)
- Inside the function, store the address of the OLD EmitSound in a global var (or local if the replacement EmitSound is in the same lua file)
- Use the meta-table from step 2 to replace EmitSound with our function from the instructions above (the callback function)
I'll show you some code in a second, but first I need to do some explaining:
- † [From step 1] Yes, the Entity:EmitSound() only has 3 arguments. The first parameter that we have above is the Entity that we want to emit the sound from. That object is automagically passed to this function by the Lua engine and represents the "self" keyword within SWEPs and SENTs. (Interesting note: This is also how compilers handle the 'this' object when compiling classes into assembly! The 'this' object is always passed as the first pointer on the stack to methods within that class.)
- †† [From step 2] On EmitSound, the volume/pitch arguments are optional, which means we'll need to create those values if they weren't passed in order to avoid 'nil' errors.
Alright, here we go:
The Code
As defined above, here is our actual callback:
function SMEmitSound(ent, path, vol, pitch)
if(vol == nil) then vol = 100 end -- 100 for 100%
if(pitch == nil) then pitch = 80 end -- We get 80 from "SNDLVL_NORM 0.8"
-- on the official valve site (cb "soundscripts")
pitch = pitch * GetConVarNumber("host_timescale")
g_oldes(ent, path, vol, pitch)
end
And the function that actually hooks EmitSound():
function HookSound()
if(g_oldes == nil) then -- If you call this twice without this if statement,
-- you'll end up getting a stack overflow, a ton of
-- errors, and absolutely no sound!
tb = FindMetaTable("Entity")
g_oldes = tb.EmitSound -- Store the original address in g_oldes
tb.EmitSound = SMEmitSound
end
end
That should do it! Just call HookSound(), turn on sv_cheats 1, and change host_timescale to something other than 1.0 (0.5 is ideal), pull out your favorite swep and let it rip!
Limitations to this Method
So, if you've spent a little time playing around with this particular slow-motion functionality, you've probably noticed that a) NPCs don't change their pitch, and b) the default weapons (weapon_rpg, weapon_crossbow, etc.) don't either.
Why this is is a very simple explanation: they don't call EmitSound(). Since GMOD's Lua is built on top of the Source Engine (which initially defines the built-in weapons and NPCs), they don't call EmitSound because they are hard coded into the game, whereas custom SENTs and SWEPs are not. Only SWEPs and SENTs that use lua will be affected by this! This includes EVERYTHING you download from Toybox or garrysmod.org (so get to downloading!)
Finally...
Alright, so obviously my habit of writing extremely long and in-depth tutorials hasn't failed me yet (sigh) so if you kept with it throughout the entire article then bravo. Not only are the techniques explained above useful for things like EmitSound, but they are also [realistically] useful to, say, log whenever a function is called (to see if your SWEP is working or if a function calls another function correctly, or to count how many times a function is called, etc.) This technique not only works with MetaTable definitions, but instead can be applied to global functions (just skip the metadata part!)
Hopefully this tut is helpful if you need it or useful/interesting if you don't! Lua is truly an incredible scripting language and I think it was the best decision for Garry to use it for the Mod!

