ID:1226646
 
Today I'm going to be introducing a programming concept to you that really helps keep your code organized, and also helps to slim down the unique object count that is increasingly becoming important until DreamMaker's compiler gets the TLC it has needed for a few years.

First, let's introduce you to the concept of hooks.

Hooks: A primer

Hooks are a way of creating modular behavior that can be modified at runtime.

Hooks are useful for situations where you want to listen for when something happens, and call subsequent behavior. Or, to define points at which unique behavior can be inserted into an object at runtime without the object having to know very much about it ahead of time.

Hooks solve a lot of problems that otherwise would be very difficult to approach.


Hooking isn't free

Yeah, it sounds nice to be able to change the behaviors of objects on the fly, but it comes with a cost. There is a small overhead to every function call, but it's usually pretty negligible in BYOND.

When it comes to DM code execution, it's best to let Native functions do what they do, and stay out of their way, so keeping that in mind, I've attempted to set up the hook system we'll be talking about with a minimal impact on your processor.

Adding Hooks should be limited to behaviors that you want to keep generalized. The more hooks you have, the worse my system will perform, so don't attempt to use it for functions that should almost always be present in all cases. The point of this system is to only process code in specialized cases.

Setting it up

Let's set up a global list to contain our hooks.

var
hook = list()


Now, we need to define how to add hooks to the list. Hooks really shouldn't be arbitrarily added or removed from the list at runtime. You really should only have to set up the lists once.

I also don't recommend removal of hooks. It should be set it and forget it.

proc
AddHook(var/hook)
if(hooks[hook]==null)
hooks[hook] = list()


Now, individual hooks are just the overall name of the hook. In order to use them, we need to hook one object up to another. Whenever an object is instructed to call a hook, it will expect to also receive the name of the hook called, the object calling the hook, and a list of arguments to be passed to the hooked function.

proc
Hook(var/hook,var/datum/trigger,var/call_obj,var/call_func)
var/list/l = hooks[hook][trigger]
if(!l)
l = list()
hooks[hook][trigger] = l
l[call_obj] = call_func
trigger.vars["hook_[hook]_refs"] = l.len


Now, we have a way of linking two objects together at a specific hook. Examining the Hook() function, we find that it looks for a list within the hook list. First, there are a number of lists referenced by the name of a specific hook, then there is a second list, which correlates to any object that references said hook. The object's list within hooks stores a list of objects hooked on that hook name to that trigger object, which is a key-value pair (associative value) with the name of the function that is called whenever the hook is encountered.

Let's just go ahead and set up a way to unhook objects.

proc
Unhook(var/hook,var/datum/trigger,var/call_obj)
var/list/l = hooks[hook][trigger]
if(!l)
return
l[call_obj] = null
l -= call_obj
trigger.vars["hook_[hook]_refs"] = l.len
if(l.len==0)
hooks[hook][trigger] = null
hooks[hook] -= trigger


Now that we've got that set up, let's start talking about a few points I haven't mentioned. You'll notice that I'm setting a variable using the vars list. This variable should be defined on any object that can use a specific hook. It will keep track of how many objects are currently linked to the hook by name [hook]. This is a way to quickly tell that we don't need to call a hook, and thus, can skip searching the global list to find linked objects.

We can set up hooks now, so let's just set up a way to call them.

proc
HookCalled(var/hook,var/datum/trigger,var/list/arguments)
var/list/l = hooks[hook][trigger]
if(l!=null)
var/datum/triggered

for(var/count=1;count<=l.len;count++)
triggered = l[count]
if(triggered!=null)
call(triggered,l[triggered])(arglist(arguments))


The call function just looks for the triggered object, then runs through a list of hooked objects, and calls the linked function on the linked object.

Using what we've learned

Now that we have the overall system completed, we can start learning how to use it in your game.

Let's say you want to make an item in-game that will, when equipped, make the player leave trails of fire wherever they walk. Obviously, we could tell every piece of equipment when the player moves (even if they don't really care to know), or we can hard-code that walk notifications will look for an item of this type, or we can do a number of things that are just wasteful and don't make sense.

Instead, our hooks system is perfect for handling a feature like this:

First, we need to set up two hook types for this system, and inject hook calls into the existing code.

mob
Move(var/atom/newloc,var/dir=0,var/step_x=0,var/step_y=0)
. = ..(newloc,dir,step_x,step_y)
if(.)
if(src.hook_Moved_refs)
HookCalled("Moved",src,args)
return .


We also need to tell the world that there is a hook named Moved:

world
New()
..()
BuildHooks()

proc
BuildHooks()
AddHook("Moved")


Now, let's take a look at the boots:

obj/item/equipment/boots/boots_88mph
//let's pretend you have an equipments system and Equipped is called whenever the mob puts on the boots
var/tmp
hook_Moved_refs = 0
Equipped(var/mob/m)
..()
Hook("Moved",m,src,"LeaveTrail")
Unequipped(var/mob/m)
..()
Unhook("Moved",m,src)
proc
LeaveTrail(var/atom/newloc,var/dir=0,var/step_x=0,var/step_y=0)
//set up code to leave a trail of fire behind the player


Now, when we run our code, we will find that the boots are notified every time the player moves, but the rest of his items aren't.

I use hooks pretty extensively in my item, action, and combat systems. I find that they really simplify and slim down my code, and while they do cost a bit in terms of overhead, you will actually find yourself saving overhead more often than not due to this system anyway.

If you come up with any creative uses for this, let me know. This is just the first of a handful of modular design tutorials I'd like to write.
Seeing as you create the variable dynamically, wouldn't if(src.hook_Moved_refs) give you an error at compile time? Wouldn't you have to do if(src.vars["hook_Moved_refs"])?
In response to Albro1
Albro1 wrote:
Seeing as you create the variable dynamically, wouldn't if(src.hook_Moved_refs) give you an error at compile time? Wouldn't you have to do if(src.vars["hook_Moved_refs"])?

No. That's a runtime list access. It wouldn't return compile-time errors at all.

However, calling your latter function would return a runtime error as well if used on an object that doesn't have that variable. Try it and see. Var lists are an internal list type, and don't work exactly the same as byond's /lists. There are a few lists in BYOND that behave out of compliance with BYOND's DM lists like this, notably savefile lists, and var lists.

Which is why we don't define that variable at runtime at all, because in order to ensure that it's safe would require a list Find() to find if the index even exists first. We define the reference count at compile time. We just access it via vars[] because the name of the hook being called is uncertain at the time of writing the hook functions.

Since we need to insert the hooked functions manually, there's no harm in defining the variables for the number of hooks per type of hook on the object within code as well.
There's quite a few src's there that don't need to be.
In response to Zecronious
That's essentially just programmer preference. I also prefer to explicitly scope the src object's variables and procedure calls with src.
In response to Zecronious
Zecronious wrote:
There's quite a few src's there that don't need to be.

I'll just do some tidying...

Good catch. I converted this from a system where I had need for multiple sets of hook lists to cut down on list searching, so these functions were defined inside of a datum.
In response to Stephen001
True, I just thought because most of the time he's not using src so maybe that's his preference to not use it where possible. You're right though, totally preference.
In response to Ter13
Oh right, that's cool.
This is really good stuff! Cheers for it, can't wait to see more.
In response to FIREking
FIREking wrote:
This is really good stuff! Cheers for it, can't wait to see more.

That means a lot coming from you. I'm currently a bit sidetracked at the moment with a contract job that has turned into a monster, but you'll see some interesting stuff coming out of me soon.
Just made a library out of this per request: http://www.byond.com/developer/Ter13/CodeHooks?tab=index

And yes, albro, I did update the library to address your concerns about the var list runtimes. The method above was brittle, and after a brief chat with Stephen001, I found a way to make it much more user-proof.
Between this and object pools you're really on fire lately. Thanks for all this.