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.